Native bindings
Call C libraries from Antril - Import declarations, larva bind with C headers and .bind descriptions, and binding profiles.
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:
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"))));
}
}
larva run
true
5
- Parameters can be numbers,
bool, pointers,func*and strings. Astrtravels as a zero-terminated copy; astr?can also benull. The result is a number, abool, a pointer or afunc*. - C structs are
valuetypes with public fields in C's order, and cross to C by pointer (SYSTEMTIME*), inunsafecode. [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.Marshalcopies 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
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:
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:
import System;
import Bindings;
Main()
{
u64 start = Kernel32.GetTickCount64();
Kernel32.Sleep(50);
Console.WriteLine(Kernel32.GetTickCount64() - start >= 40);
}
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:
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 (
-Iadds include folders; on Windows the SDK's and MSVC's are added for you).--no-ppreads it as it is. - Structs and unions become
valuetypes with C's layout,#defineand enum constants become constants,const char*becomesstr, and function pointers becomefunc*<…> : R. - Functions the library does not export are left out, and so are those that cannot be called (variable arguments, structs by value).
--describewrites what was read as a.bindfile, which you can edit and bind again.
A struct from the header is used by pointer, in unsafe code:
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:
larva bind --profile SDLBindingProfile.ant
This is the profile of the SDL3 package:
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:
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:
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.
Edit this page on GitHub Report an issue