Skip to content

Commit

Permalink
First!
Browse files Browse the repository at this point in the history
  • Loading branch information
MichalStrehovsky committed Jun 24, 2021
1 parent 738d64e commit 0a2d0a2
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 2 deletions.
110 changes: 108 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,108 @@
# bflat
C# as you know it but with Go-inspired tooling (small, selfcontained, and native executables)
# ♭ bflat
C# as you know it but with Go-inspired tooling that produces small, selfcontained, and native executables out of the box.

```console
$ echo 'System.Console.WriteLine("Hello World");' > hello.cs
$ bflat build hello.cs
$ ./hello
Hello World
$ bflat build hello.cs --os:windows
$ file ./hello.exe
hello.exe: PE32+ executable (console) x86-64, for MS Windows
```

## 🎻 What exactly is bflat

bflat is a concoction of Roslyn - the "official" C# compiler that produces .NET executables - and NativeAOT (née CoreRT) - the experimental ahead of time compiler for .NET based on CoreCLR's crossgen2. Thanks to this, you get access to the latest C# features using the high performance CoreCLR GC and native code generator (RyuJIT).

bflat merges the two components together into a single ahead of time crosscompiler and runtime for C#.

bflat can currently target:

* x64 and ARM64 Linux
* x64 and ARM64 Windows

It can either produce native executables, or native shared libraries that can be called from other languages through FFI.

## 🥁 Where to get bflat

Look at the Releases tab of this repo and download a compiler that matches your host system. These are all crosscompilers and can target any of the supported OSes/architectures.

Unzip the archive to a convenient location and add the root to your PATH. You're all set. See the samples directory for a couple samples.

## 🎷 I don't see dotnet, MSBuild, or NuGet

That's the point. bflat is to dotnet as VS Code is to VS.

## 🎙 Where is the source code

The source code is in the respective Roslyn/NativeAOT repositories. I'm not ready for people to see (or to accept pull requests) things that are specific to bflat. If you think bflat is useful, you can leave me a tip in my [tip jar](https://paypal.me/MichalStrehovsky) and include your GitHub user name in a note so that I can give you access to a private repo when I'm ready.

## 📻 How to stay up-to-date on bflat?

Follow me on [Twitter](https://twitter.com/MStrehovsky).

## 🎺 Optimizing output for size

By default, bflat produces executables that are between 2 MB and 3 MB in size, even for the simplest apps. There are multiple reasons for this:

* bflat includes stack trace data about all compiled methods so that it can print pretty exception stack traces
* even the simplest apps might end up calling into reflection (to e.g. get the name of the `OutOfMemoryException` class), globalization, etc.
* method bodies are aligned at 16-byte boundaries to optimize CPU cache line utilization
* (Doesn't apply to Windows) DWARF debug information is included in the executable

The "bigger" defaults are chosen for friendliness and convenience. To get an experience that more closely matches low level programming languages, specify `--no-reflection`, `--no-stacktrace-data`, `--no-globalization`, and `--no-exception-messages` arguments to `bflat build`.

Best to show an example. Following program:

```csharp
using System.Diagnostics;
using static System.Console;

WriteLine($"NullReferenceException message is: {new NullReferenceException().Message}");
WriteLine($"The runtime type of int is named: {typeof(int)}");
WriteLine($"Type of boxed integer is{(123.GetType() == typeof(int) ? "" : " not")} equal to typeof(int)");
WriteLine($"Type of boxed integer is{(123.GetType() == typeof(byte) ? "" : " not")} equal to typeof(byte)");
WriteLine($"Upper case of 'Вторник' is '{"Вторник".ToUpper()}'");
WriteLine($"Current stack frame is {new StackTrace().GetFrame(0)}");
```

will print this by default:

```
NullReferenceException message is: Object reference not set to an instance of an object.
The runtime type of int is named: System.Int32
Type of boxed integer is equal to typeof(int)
Type of boxed integer is not equal to typeof(byte)
Upper case of 'Вторник' is 'ВТОРНИК'
Current stack frame is <Program>$.<Main>$(String[]) + 0x154 at offset 340 in file:line:column <filename unknown>:0:0
```

But it will print this with all above arguments specified:

```
NullReferenceException message is: Arg_NullReferenceException
The runtime type of int is named: EETypeRva:0x00048BD0
Type of boxed integer is equal to typeof(int)
Type of boxed integer is not equal to typeof(byte)
Upper case of 'Вторник' is 'Вторник'
Current stack frame is ms!<BaseAddress>+0xb82d4 at offset 340 in file:line:column <filename unknown>:0:0
```

With all options turned on, one can comfortably fit useful programs under 1 MB. The above program is 735 kB on Windows at the time of writing this. The output executables are executables like any other. You can use a tool like UPX to compress them further (to ~300 kB range).

If you're on a Unix-like system, you might want to run `strip` tool to remove debug information from the executable. Windows places the debug information in a separate PDB file and `strip` is not needed.

## 🎸 Preprocessor definitions

Besides the preprocessor definitions provided at the command line, bflat defines several other symbols: `BFLAT` (defined always), `DEBUG` (defined when not optimizing), `WINDOWS`/`LINUX`/`MACOS` (when the corresponding operating system is the target), `X86`/`X64`/`ARM`/`ARM64` (when the corresponding architecture is targeted).

## 🎹 Debugging bflat apps

Apps compiled with bflat debug same as any other native code. Launch the produced executable under your favorite debugger (gdb or lldb on Linux, or Visual Studio or WinDbg on Windows) and you'll be able to set breakpoints, step, and see local variables.

## 😢 Current known issues

* Things that depend on libssl don't work on Linux - this includes a lot of crypto. We need to link to libssl statically but the framework code is structured very much against that. Needs a bit work.
* Globalization is uncondionally disabled on Linux - we need to link with ICU. Needs a bit work.
* No Linux host of the bflat compiler. This is unforunate fallout from libssl. Apparently the C# compiler insists on calculating SHA hashes of files and we need a working libssl for that.
19 changes: 19 additions & 0 deletions samples/DynamicLibrary/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Shared library sample

To build a shared library run following command:

```console
$ bflat build library.cs
```

Bflat automatically detects you're trying to build a library because there's no Main. You can also specify `--target` to bflat.

This will produce a library.so file on Linux and library.dll on Windows.

The library can be consumed from any other programming language. Since we're using C#, let's consume it from C#. Because at this point library.so/.dll is a native library like any other, we need to p/invoke into it.

```console
$ bflat build libraryconsumer.cs
```

This will build a libraryconsumer(.exe) binary that will load the library and invoke functions in it.
38 changes: 38 additions & 0 deletions samples/DynamicLibrary/library.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Runtime.InteropServices;

internal static class Library
{
[UnmanagedCallersOnly(EntryPoint = "Add")]
private static int Add(int num1, int num2)
{
return num1 + num2;
}

// Note that parameters to UnmanagedCallersOnly methods may only be primitive types (except bool),
// pointers and structures consisting of the above. Reference types are not permitted because they don't
// have an ABI representation on the native side.
[UnmanagedCallersOnly(EntryPoint = "CountCharacters")]
private static unsafe nint CountCharacters(byte* pChars)
{
byte* pCurrent = pChars;
while (*pCurrent++ != 0) ;
return (nint)(pCurrent - pChars) - 1;
}

// Note that it is not allowed to leak exceptions out of UnmanagedCallersOnly methods.
// There's no ABI-defined way to propagate the exception across native code.
[UnmanagedCallersOnly(EntryPoint = "TryDivide")]
private static unsafe int TryDivide(int num1, int num2, int* result)
{
try
{
*result = num1 / num2;
}
catch (DivideByZeroException ex)
{
return 0;
}
return 1;
}
}
15 changes: 15 additions & 0 deletions samples/DynamicLibrary/libraryconsumer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Runtime.InteropServices;
using static System.Console;

WriteLine($"Add(1, 2) = {Add(1, 2)}");
WriteLine($"CountCharacters(\"Hello\") = {CountCharacters("Hello")}");
WriteLine($"TryDivide(1, 0, out _) = {TryDivide(1, 0, out _)}");

[DllImport("library")]
static extern int Add(int num1, int num2);

[DllImport("library", CharSet = CharSet.Ansi)]
static extern nint CountCharacters(string s);

[DllImport("library")]
static extern bool TryDivide(int num1, int num2, out int result);
11 changes: 11 additions & 0 deletions samples/HelloWorld/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Hello world sample

This is a basic hello world with a twist that if you run it with an argument, it will print Hello $Argument.

To build:

```console
$ bflat build hello.cs
```

This will produce a hello(.exe) file that is native compiled.
10 changes: 10 additions & 0 deletions samples/HelloWorld/hello.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

if (args.Length == 0)
{
Console.WriteLine("Hello world!");
}
else
{
Console.WriteLine($"Hello {args[0]}!");
}
11 changes: 11 additions & 0 deletions samples/MinimalSize/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Minimal size sample

This demonstrates how to build a minimal size executable with bflat.

```console
$ bflat build minimalsize.cs --no-reflection --no-stacktrace-data --no-globalization --no-exception-messages --Os
```

This will produce a minimalsize(.exe) file that is native compiled. You can launch it. Observe the difference in runtime behavior and size of the output when you omit some of the arguments from the `bflat build` command line above.

Note that if you are running on a Unix-like system, you might want to run the `strip` tool on the native executable to remove the debugging symbols that make the executable larger than it needs to be.
9 changes: 9 additions & 0 deletions samples/MinimalSize/minimalsize.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;
using System.Diagnostics;

Console.WriteLine($"NullReferenceException message is: {new NullReferenceException().Message}");
Console.WriteLine($"The runtime type of int is named: {typeof(int)}");
Console.WriteLine($"Type of boxed integer is{(123.GetType() == typeof(int) ? "" : " not")} equal to typeof(int)");
Console.WriteLine($"Type of boxed integer is{(123.GetType() == typeof(byte) ? "" : " not")} equal to typeof(byte)");
Console.WriteLine($"Upper case of 'Вторник' is '{"Вторник".ToUpper()}'");
Console.WriteLine($"Current stack frame is {new StackTrace().GetFrame(0)}");

0 comments on commit 0a2d0a2

Please sign in to comment.