Native bindings

Call C libraries from Antril - Import declarations, larva bind with C headers and .bind descriptions, and binding profiles.

Last updated 10 min read Edit on GitHub

On this page

Antril calls functions of native libraries (.dll, .so, .dylib) directly. You can declare each function yourself, or let larva bind write a whole binding from a C header.

Declaring a C function

[Import("library")] on a function without a body makes it a call into the library. A second argument gives the C name when the Antril name is different:

Antril
import System;
import System.Runtime.InteropServices;

into namespace Native
{
	[Import("kernel32.dll")]
	GetCurrentProcessId() : u32;

	[Import("kernel32.dll", "lstrlenW")]
	WideLength(String<u16> text) : i32;

	Main()
	{
		Console.WriteLine(GetCurrentProcessId() > 0);
		Console.WriteLine(WideLength(new String<u16>(Unicode.ToUtf16("hello"))));
	}
}
Terminal
larva run
true
5
  • Parameters can be numbers, bool, pointers, func* and strings. A str travels as a zero-terminated copy; a str? can also be null. The result is a number, a bool, a pointer or a func*.
  • C structs are value types with public fields in C's order, and cross to C by pointer (SYSTEMTIME*), in unsafe code.
  • [ValueLayout(Layout.Explicit)] with [FieldOffset(n)] lays out unions, and [ValueLayout(Layout.Sequential, Pack = 1)] packed structs.
  • Functions with a variable number of arguments, and structs passed by value, cannot be called.
  • [Export("name")] on a public static function gives it that C name, so C code can call it.
  • System.Runtime.InteropServices.Marshal copies strings and memory to and from native memory.

larva bind

larva bind reads a native library's exports and a description of its API, and writes a binding: a Larva library (.dll) of [Import] declarations. With --source, it also writes the binding as Antril source, to read.

larva bind <lib.dll|lib.so> <api.bind|api.h> [-o <Name.dll>] [--source Name.ant] [--describe api.bind]
           [--no-pp] [-I <dir>]... [--class Name] [--core <dir>]
larva bind --list <lib.dll|lib.so>
larva bind --profile <Profile.ant|folder> [<lib>...] [<api.h>...] [-I <dir>]... [-o <Name.dll>] [--source <folder>]
           [--class Name] [--dry-run] [--timings] [--core <dir>]

larva bind --list kernel32.dll prints the names a library exports.

From a .bind description

A .bind file lists the functions you want, in Antril's types:

# A few of kernel32's functions.
library kernel32.dll
GetCurrentProcessId() : u32
GetTickCount64() : u64
Sleep(u32 milliseconds)
Beep as MakeBeep(u32 frequency, u32 duration) : bool
Terminal
larva bind kernel32.dll kernel32.bind --source Kernel32.ant
Kernel32Bindings.dll: Kernel32 with 4 function(s) of kernel32.dll
Kernel32.ant: Antril source

Kernel32.ant shows what the binding holds:

Antril
into namespace Bindings
{
	/// Functions of kernel32.dll, called directly.
	public static class Kernel32
	{
		public:
			[Import("kernel32.dll")]
			GetCurrentProcessId() : u32;

			[Import("kernel32.dll")]
			GetTickCount64() : u64;

			[Import("kernel32.dll")]
			Sleep(u32 milliseconds);

			[Import("kernel32.dll", "Beep")]
			MakeBeep(u32 frequency, u32 duration) : bool;
	}
}

A program uses the library by passing it to larva build:

Antril
import System;
import Bindings;

Main()
{
	u64 start = Kernel32.GetTickCount64();
	Kernel32.Sleep(50);
	Console.WriteLine(Kernel32.GetTickCount64() - start >= 40);
}
Terminal
larva build -o app.exe main.ant Kernel32Bindings.dll
app.exe (llvm back end, O3, entry app::$Module.Main)
app.exe
true

What a .bind file can say, one declaration per line:

Line What it declares
library kernel32.dll The native library (required).
namespace Win, class Kernel32, assembly Win.Bindings Where the declarations go (by default namespace Bindings, a class named after the library, and an assembly named after the class plus Bindings).
file Time The source file the next declarations are written to.
Name(T a, U b) : R A function.
Entry as Name(…) : R A function with another name in Antril.
struct Name [pack N] { T field; … }, union Name { … } A C struct or union.
enum Name : u32 [flags] { A = 1, B = 2 } An enum (flags: a bitflag enum).
const NAME = 7 An integer constant.
# … A comment.

From a C header

Give a header instead of a .bind file and larva reads the C declarations:

Terminal
larva bind kernel32.dll api.h --class Kernel32 -o Win.Bindings.dll --source Win.ant --describe win.bind
Win.Bindings.dll: Kernel32 with 2 function(s) of kernel32.dll, 1 struct(s)
Win.ant: Antril source
  • With clang installed, the header is preprocessed first (-I adds include folders; on Windows the SDK's and MSVC's are added for you). --no-pp reads it as it is.
  • Structs and unions become value types with C's layout, #define and enum constants become constants, const char* becomes str, and function pointers become func*<…> : R.
  • Functions the library does not export are left out, and so are those that cannot be called (variable arguments, structs by value).
  • --describe writes what was read as a .bind file, which you can edit and bind again.

A struct from the header is used by pointer, in unsafe code:

Antril
import System;
import Bindings;

Main()
{
	SYSTEMTIME now;
	unsafe { Kernel32.GetSystemTime(&now); }
	Console.WriteLine(now.wYear);
}

Binding profiles

A large library needs decisions: which declarations to keep, how to name them, where to put them. A binding profile is an Antril class that makes them. It derives from System.Bindings.BindingProfile, and larva compiles it and runs it while it binds:

Terminal
larva bind --profile SDLBindingProfile.ant

This is the profile of the SDL3 package:

Antril
import System;
import System.Bindings;

into namespace SDL.Bindings.Internal
{
	/// SDL3 for Larva: a namespace, a class and a file per header (SDL.Init, SDL.Video…), names in Pascal case
	/// without `SDL_`, and the init and window flags as bitflag enums.
	public class SDLBindingProfile : BindingProfile
	{
		public:
			override Configure(BindingSettings s)
			{
				// What is read (paths from this file's folder).
				s.Libraries.Add("build/SDL3.dll");
				s.Headers.Add("include/SDL3/SDL.h");
				s.Includes.Add("include");

				// Where it goes.
				s.Namespace = "SDL";
				s.Class = "SDL";
				s.Assembly = "SDL.Bindings";
				s.Output = "SDL.Bindings.dll";
				s.Source = "bindings";

				// Names: no `SDL_`, Pascal case (parameters in camel case).
				s.Strip.Add("SDL_");
				s.TypeCase = NameCase.Pascal;
				s.FunctionCase = NameCase.Pascal;
				s.ConstantCase = NameCase.Pascal;
				s.FieldCase = NameCase.Pascal;
				s.ParameterCase = NameCase.Camel;

				// One namespace, class and file per header: SDL_video.h → SDL.Video, class Video, Video.ant.
				s.NamespaceByHeader = true;
				s.ClassByHeader = true;
				s.Split = SplitBy.Header;
			}

			override Start(BindingModel m)
			{
				// Groups of #defines as typed enums: what C wrote with the typedef takes the enum.
				m.MakeEnum("InitFlags", "SDL_INIT_*", true).Typedef("SDL_InitFlags");
				m.MakeEnum("WindowFlags", "SDL_WINDOW_*", "SDL_WINDOW_SURFACE_*", true).Typedef("SDL_WindowFlags");
				m.MakeEnum("Keycode", "SDLK_*", false).Typedef("SDL_Keycode");
				m.MakeEnum("Keymod", "SDL_KMOD_*", true).Typedef("SDL_Keymod");
			}

			// Leaves out what you do not want in the binding.
			override Function(CFunction f) : bool => !f.Entry.StartsWith("SDL_iconv");

			// Specific renames; the rest keeps the name the settings propose.
			override Name(NameKind kind, str cName, str proposed) : str =>
				cName == "SDL_Init" ? "Initialize" : proposed;
	}
}

With it, SDL's C API reads like this from Antril:

Antril
import SDL.Init;
import SDL.Events;

unsafe Main() : i32
{
	if (!Init.Initialize(InitFlags.Video | InitFlags.Events))
		return 1;
	Event e;
	loop (Events.PollEvent(&e))
	{
		if (e.Type == (u32)EventType.Quit)
			break;
	}
	Init.Quit();
	return 0;
}

The hooks

larva calls the profile's methods in this order:

Method When What it decides
Configure(BindingSettings s) Once, before the headers are read. What is read and the rules for every name.
Start(BindingModel m) With everything read. Enums made from groups of constants (m.MakeEnum("Keycode", "SDLK_*", false)), with their typedefs.
Enum, Struct, Constant, Function For each declaration. false leaves it out. The hook can also change its name, types, namespace, class or file.
Name(NameKind kind, str cName, str proposed) For each name no hook fixed. The final name.
Finish(BindingModel m) With the whole binding. Last changes.

If two declarations end up with the same name in the same place, the second keeps its C name and larva warns.

The settings

Setting What it sets
Libraries, Headers, Includes The native libraries (the first is imported from), the headers, and the include folders. Libraries and headers on the command line replace these.
Namespace, Class, Assembly The root namespace (default Bindings), the class of functions and constants (default: the library's name), and the assembly's name (default: the class plus Bindings).
Output, Source The library written, and the folder of the Antril source (empty: none).
Split How the source is split: SplitBy.None, Header, Namespace or Kind.
NamespaceByHeader, ClassByHeader, HeaderStrip A namespace and a class per header, and prefixes taken from header names.
Strip, StripTypes, StripFunctions, StripConstants Prefixes taken off C names.
TypeCase, FunctionCase, ConstantCase, FieldCase, ParameterCase, MemberCase The case of each kind of name: NameCase.Keep, Pascal, Camel, Upper, Snake.
StripMemberPrefix Enum members lose the prefix they share.
EnumsFromC, AutoFlags C enums become enums; enums of distinct bits become bitflag enums.
Exports A declared function the library does not export is left out (ExportCheck.Keep) or is an error (Check).
LongBits, AllHeaders The size of C's long (32 on Windows, 64 on Linux and macOS); whether system headers' declarations are kept too.

--dry-run runs the profile without writing anything, and --timings shows each step. The editor analyzes a profile like any Antril file, with System.Bindings available.

Using a binding in a project

A project names the compiled binding with assembly: and the native library with lib: in its DEPS chunk. The native library is copied next to the program. Leave the profile and the generated source out of the build:

.queen
chunk SOURCES:
   exclude: [bindings, SDLBindingProfile.ant]

chunk DEPS:
   assembly: SDL.Bindings.dll,
   lib: build/SDL3.dll

A binding never has the file name of the native library it wraps: Windows would load one in place of the other. That is why larva bind names it <Name>Bindings.dll or <Name>.Bindings.dll, and refuses an -o that matches the native library.

To share a binding, publish it as a Hive package.

Was this page helpful?

No address is stored; your user name is, if you are signed in.

Esc