Interop with C

Calling C libraries with [Import], exporting functions with [Export], strings that cross, func* function pointers, structs and unions with [ValueLayout], and native memory with Marshal.

Last updated 8 min read Edit on GitHub

On this page

Antril calls C functions in native libraries (.dll, .so), and C can call Antril functions. What crosses is what C holds: numbers, bool, pointers, function pointers, and strings copied for the call. Most of it uses pointers, so it lives in unsafe code.

To make the declarations for a whole C library from its headers, use larva bind instead of writing them by hand.

Importing a C function

[Import("library")] on a function without a body declares a C function of that library. The C name is the function's name, or the second argument: [Import("kernel32.dll", "lstrlenW")].

Antril
import System;
import System.Runtime.InteropServices;

[Import("kernel32.dll")] GetTickCount64() : u64;
[Import("kernel32.dll", "lstrlenW")] WideLength(String<u16> text) : i32;
[Import("kernel32.dll", "lstrlenA")] Utf8Length(str text) : i32;
[Import("ucrtbase.dll")] qsort(void* items, u64 count, u64 size, func*<void*, void*> : i32 compare);
[Import("ucrtbase.dll")] getenv(str name) : u8*;

unsafe Compare(void* a, void* b) : i32 => *(i32*)a - *(i32*)b;

Main()
{
	Console.WriteLine(GetTickCount64() > 0);
	Console.WriteLine(WideLength(new String<u16>(Unicode.ToUtf16("año"))));
	Console.WriteLine(Utf8Length("año"));
	i32[] data = [4, 1, 3, 2];
	unsafe
	{
		qsort(&data[0], 4, 4, &Compare);
		u8* value = getenv("OS");
		Console.WriteLine(Marshal.PtrToStringUTF8(value));
	}
	Console.WriteLine(data[0] + "," + data[1] + "," + data[2] + "," + data[3]);
}

On Windows:

Terminal
larva run
true
3
4
Windows_NT
1,2,3,4
  • The library is loaded, and the function found, the first time it is called: there is nothing to link.
  • An imported function is static, has no body and no type parameters, and is not virtual (AN1051). It names its library (AN1052).
  • Imported functions can be free functions or members of a type, and can be called from safe code; only pointer arguments need an unsafe block.

Import and Export are read by the compiler by name, so they need no import. System.Runtime.InteropServices holds Marshal, NativeMemory and the layout attributes.

What crosses

Antril C As a parameter As a result
i8 … i64, u8 … u64, f32, f64, iptr, uptr the same width yes yes
bool a one-byte bool yes yes
T*, void* a pointer yes yes
func*<A> : R a function pointer yes yes
str const char*: a temporary null-terminated UTF-8 copy yes no
String<u16> const wchar_t* on Windows: a temporary null-terminated UTF-16 copy yes no
str?, String<u16>? the same, or NULL for null yes no

Anything else is an error: objects, arrays and value types as parameters (AN1053) or results (AN1054). The string copies live only during the call; C must not keep them. A string C returns comes back as a u8* (or u16*), read with Marshal.PtrToStringUTF8 or Marshal.PtrToStringUni.

str is UTF-8 text. String<u16> is UTF-16 text: make one from a str with new String<u16>(Unicode.ToUtf16(text)).

Structs

A C struct is a value with the same fields, in the same order. Antril lays out a value as C does by default (in order, each field at its natural alignment), so it crosses by pointer as it is:

Antril
import System;

public value SystemTime
{
	public:
		u16 Year;
		u16 Month;
		u16 DayOfWeek;
		u16 Day;
		u16 Hour;
		u16 Minute;
		u16 Second;
		u16 Milliseconds;
}

[Import("kernel32.dll")] GetSystemTime(SystemTime* time);

Main()
{
	SystemTime now;
	unsafe { GetSystemTime(&now); }
	Console.WriteLine(now.Year > 2000);
}
Terminal
larva run
true

A struct crosses only by pointer. By value it can only be a field of another struct.

Warning

Not yet available: passing or returning a struct by value to or from C.

Layouts

[ValueLayout] and [FieldOffset] (in System.Runtime.InteropServices) say where each field goes, for structs and unions that need another layout:

Attribute What it does
[ValueLayout(Layout.Sequential)] Fields in order, each at its natural alignment: the default layout, written out.
[ValueLayout(Layout.Sequential, Pack = n)] The same, with no field aligned to more than n bytes (0, 1, 2, 4, 8 or 16; 0 means each field's own).
[ValueLayout(Layout.Explicit)] Each field where its [FieldOffset(n)] says, in bytes from the start. Fields may overlap: a C union has every field at offset 0.
Size = n Makes the value at least n bytes.
AntrilRun
import System;
import System.Runtime.InteropServices;

[ValueLayout(Layout.Explicit)]
public value Number
{
	public:
		[FieldOffset(0)] i64 Whole;
		[FieldOffset(0)] f64 Real;
		[FieldOffset(4)] u32 High;
}

[ValueLayout(Layout.Sequential, Pack = 1)]
public value Packed
{
	public:
		u8 Tag;
		u32 Length;
		fixed u8[3] Name;
}

[ValueLayout(Layout.Sequential, Size = 32)]
public value Padded
{
	public:
		i32 A;
}

public value Natural
{
	public:
		u8 Tag;
		u32 Length;
}

Main()
{
	Number n;
	n.Real = 1.0;
	Console.WriteLine(n.High);
	Console.WriteLine(sizeof(Number));
	Console.WriteLine(sizeof(Packed));
	Console.WriteLine(sizeof(Padded));
	Console.WriteLine(sizeof(Natural));
}
Terminal
larva run
1072693248
8
8
32
8

n.High reads the upper half of the bits of 1.0. Such a value is copied, passed by pointer, nested and zeroed like any other.

Code When
AN1080 [ValueLayout] on something other than a value without type parameters.
AN1081 A field of a laid-out value holds objects. Fields are numbers, bools, pointers, func*, fixed buffers and other such values.
AN1082 [FieldOffset] without Layout.Explicit, or a field of an explicit value without one.
AN1083 Pack is not 0, 1, 2, 4, 8 or 16.

Function pointers

func*<A, B> : R is the address of a function that C can call: no captured object, the C calling convention, and only C types as parameters and result (AN1057). Without : R it returns nothing.

  • &F gives the address of a static, non-generic function F; when F is overloaded, the expected func* type picks the overload (AN2118 when none fits). Its parameters and result must be C types (AN2119).
  • Calling a func* runs code at an address, so it goes in unsafe code (AN2120).
AntrilRun
import System;

Scale(i32 x, i32 y) : i32 => x * y + 1;

Main()
{
	unsafe
	{
		func*<i32, i32> : i32 scale = &Scale;
		Console.WriteLine(scale(6, 7));
	}
}
Terminal
larva run
43

The qsort call above passes &Compare to C, which calls it back.

Exporting a function

[Export] or [Export("name")] on a static function with a body makes it a C symbol of the program or library, under its own name or the one given. It takes and gives only C types: numbers, bool, pointers and func* (AN1056); it has no type parameters and is not virtual (AN1055).

AntrilRun
[Export("antril_scale")]
Scale(i32 x, i32 y) : i32 => x * y + 1;

Main()
{
}

Native memory

System.Runtime.InteropServices has what moves data between Antril and C memory. All of it is unsafe.

Member What it does
Marshal.AllocHGlobal(bytes), Marshal.FreeHGlobal(p) Zeroed native memory, and giving it back.
Marshal.AllocCoTaskMem(bytes), Marshal.FreeCoTaskMem(p) The same.
Marshal.StringToHGlobalUTF8(text) A null-terminated UTF-8 copy of text in native memory (a u8*).
Marshal.StringToHGlobalUni(text) A null-terminated UTF-16 copy (a u16*).
Marshal.PtrToStringUTF8(p), Marshal.PtrToStringUTF8(p, length) The UTF-8 text at p (null-terminated, or length bytes) as a str; null gives "".
Marshal.PtrToStringUni(p), Marshal.PtrToStringUni(p, length) The same for UTF-16.
Marshal.ReadByte, ReadInt16, ReadInt32, ReadInt64, ReadIntPtr Reads at an address, with an optional byte offset.
Marshal.WriteByte, WriteInt16, WriteInt32, WriteInt64, WriteIntPtr Writes at an address, with an optional byte offset.
Marshal.Copy(array, start, p, length), Marshal.Copy(p, array, start, length) Copies numbers between an array and native memory.
Marshal.SizeOf<T>() The size of a T in bytes.
NativeMemory.Alloc(bytes), AllocZeroed(count, size), Free(p), Copy(source, destination, bytes) Native blocks.
Memory.Copy(target, source, count), Memory.Compare(left, right, count) Copies or compares bytes.

Native memory is not seen by the collector: give back what you take.

Next: Attributes.

Was this page helpful?

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

Esc