Skip to content

.NET integration

ghūl is hosted on and targets .NET 10 and can consume most types in .NET assemblies built with C#.

projects

The ghūl compiler is driven by MSBuild and uses the .NET SDK targets for most of the build process. Provided you reference the ghūl runtime library package, things should work as you'd expect for any other .NET SDK project. You can add package references, build assemblies and pack NuGet packages etc. all using the normal dotnet command line tools.

name mangling

When consuming C# code the ghūl compiler transforms symbol names to better match ghūl conventions:

  • Class, struct and trait (=interface) names are left unchanged
  • Any generic type argument count suffix is left as-is (for example KeyValuePair`2)
  • Enum names and enum member names are transformed to MACRO_CASE
  • Method, property and field names are transformed to snake_case
  • Names that conflict with ghūl keywords are prefixed with `

namespace and type name re-mapping

Some commonly used namespace and type names are re-mapped in line with ghūl conventions

namespaces

  • System.Collections.Generic is mapped to Collections
  • System.IO is mapped to IO

framework and collection types

Original TypeMapped Type
System.IDisposableGhul.Disposable
System.ConsoleIO.Std
System.Collections.IEnumerableCollections.NonGenericIterable
System.Collections.Generic.IReadOnlyCollectionCollections.Bag
System.Collections.Generic.ICollectionCollections.MutableBag
System.Collections.IEnumeratorCollections.MoveNext
System.Collections.Generic.IEnumerableCollections.Iterable
System.Collections.Generic.IEnumeratorCollections.Iterator
System.Collections.Generic.IReadOnlyListCollections.List
System.Collections.Generic.IListCollections.MutableList
System.Collections.Generic.ListCollections.LIST
System.Collections.Generic.IReadOnlyDictionaryCollections.Map
System.Collections.Generic.IDictionaryCollections.MutableMap
System.Collections.Generic.DictionaryCollections.MAP
System.Collections.Generic.HashSetCollections.SET
System.Collections.Generic.StackCollections.STACK
System.Threading.Tasks.TaskTasks.TASK
System.Threading.Tasks.Task<T>Tasks.TASK[T]

primitive types

Original TypeMapped Type
System.VoidGhul.void
System.BooleanGhul.bool
System.CharGhul.char
System.ByteGhul.ubyte
System.SByteGhul.byte
System.UInt16Ghul.ushort
System.Int16Ghul.short
System.UInt32Ghul.uint
System.Int32Ghul.int
System.UInt64Ghul.ulong
System.Int64Ghul.long
System.UIntPtrGhul.uword
System.IntPtrGhul.word
System.SingleGhul.single
System.DoubleGhul.double
System.DecimalGhul.decimal
System.ObjectGhul.object
System.StringGhul.string

ASP.NET Core

ASP.NET Core minimal APIs work from ghūl. Extension methods aren't exposed as members, so the fluent builder calls go through the |> thread-first operator, which passes the left-hand side as the called method's first argument:

ghul
entry(args: string[]) is
let builder = WebApplication.create_builder(args);
let app = builder.build();
// '|>' threads app in as map_get's first argument:
app |> map_get("/hello", () => Results.ok("hello, world"));
app.run(null);
si

app |> map_get(...) calls the MapGet extension on app; the route handler is an anonymous function returning an IResult.

Controller-style APIs rely on attributes, which apply to classes and methods: [ApiController], [Route(...)], [HttpGet(...)] and so on. ghūl doesn't yet place attributes on method parameters, so parameter-binding attributes like [FromBody] aren't expressible; minimal APIs bind by position and need none of them.

Entity Framework Core

Entity Framework Core works from ghūl. A context extends DbContext and exposes each table as a DbSet; EF Core's conventions expect PascalCase names, so @IL.name maps the ghūl members onto them:

ghul
// @IL.name maps these onto the PascalCase names EF Core's conventions expect.
@IL.name("Product")
class PRODUCT is
@IL.name("Id")
id: int public;
@IL.name("Name")
name: string public;
init() is si
si
class STORE_CONTEXT: DbContext is
@IL.name("Products")
products: DbSet[PRODUCT];
init(options: DbContextOptions) is
super.init(options);
si
si
add_product(context: STORE_CONTEXT, product: PRODUCT) -> Tasks.TASK is
context.products.add(product);
await context.save_changes_async(System.Threading.CancellationToken.none);
return;
si

The Products set and the entity's Id and Name are the names EF Core's model builder and SQL generation look for. Reads and writes call the async methods directly, with await - save_changes_async here.

mocking with NSubstitute

The .NET base libraries include no mocking framework; NSubstitute is the lowest-friction third-party option from ghūl, and the compiler's own test suite uses it. Substitute.for builds a stand-in for a trait, and the Returns extension stubs a call through |>:

ghul
trait Clock is
now() -> System.DateTime;
si
test_uses_a_stubbed_clock() static is
// Substitute.for takes the constructor arguments as an object[]; a
// trait has none, so pass an empty array.
let clock = Substitute.`for[Clock]([]);
// stub a return value for a call:
clock.now() |> returns(System.DateTime(2020, 1, 1, 9, 0, 0), null);
IO.Std.write_line("stubbed hour is {clock.now().hour}");
si

for is a reserved word, so the example escapes it with a backtick. Its argument is the substitute's constructor arguments as an object[]; a trait has none, so the argument is an empty array. Where a full framework isn't warranted, a hand-written trait implementation is the zero-dependency alternative.