Bindings nativos

Llama a bibliotecas de C desde Antril - declaraciones Import, larva bind con encabezados de C y descripciones .bind, y perfiles de bindings.

Última actualización: 10 min de lectura Editar en GitHub

En esta página

Antril llama directamente a funciones de bibliotecas nativas (.dll, .so, .dylib). Puedes declarar cada función tú mismo, o dejar que larva bind escriba un binding completo a partir de un encabezado de C.

Declarar una función de C

[Import("library")] en una función sin cuerpo la convierte en una llamada a la biblioteca. Un segundo argumento indica el nombre en C cuando el nombre en Antril es distinto:

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
  • Los parámetros pueden ser números, bool, punteros, func* y cadenas. Un str viaja como una copia terminada en cero; un str? también puede ser null. El resultado es un número, un bool, un puntero o un func*.
  • Las estructuras de C son tipos value con campos públicos en el orden de C, y pasan a C por puntero (SYSTEMTIME*), en código unsafe.
  • [ValueLayout(Layout.Explicit)] con [FieldOffset(n)] define la disposición de las uniones, y [ValueLayout(Layout.Sequential, Pack = 1)], la de las estructuras empaquetadas.
  • No se pueden llamar funciones con un número variable de argumentos ni pasar estructuras por valor.
  • [Export("name")] en una función estática pública le da ese nombre en C, para que el código C pueda llamarla.
  • System.Runtime.InteropServices.Marshal copia cadenas y memoria hacia y desde la memoria nativa.

larva bind

larva bind lee las exportaciones de una biblioteca nativa y una descripción de su API, y escribe un binding: una biblioteca de Larva (.dll) con declaraciones [Import]. Con --source, también escribe el binding como código fuente de Antril, para que lo puedas leer.

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 muestra los nombres que exporta una biblioteca.

Desde una descripción .bind

Un archivo .bind lista las funciones que quieres, con los tipos de Antril:

# 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 muestra lo que contiene el binding:

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;
	}
}

Un programa usa la biblioteca pasándola a 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

Lo que puede decir un archivo .bind, una declaración por línea:

Línea Qué declara
library kernel32.dll La biblioteca nativa (obligatoria).
namespace Win, class Kernel32, assembly Win.Bindings Dónde van las declaraciones (por defecto, el espacio de nombres Bindings, una clase con el nombre de la biblioteca y un ensamblado con el nombre de la clase más Bindings).
file Time El archivo fuente en el que se escriben las declaraciones siguientes.
Name(T a, U b) : R Una función.
Entry as Name(…) : R Una función con otro nombre en Antril.
struct Name [pack N] { T field; … }, union Name { … } Una estructura o unión de C.
enum Name : u32 [flags] { A = 1, B = 2 } Un enum (flags: un enum de banderas de bits).
const NAME = 7 Una constante entera.
# … Un comentario.

Desde un encabezado de C

Si en lugar de un archivo .bind le das un encabezado, larva lee las declaraciones de C:

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
  • Si tienes clang instalado, primero se preprocesa el encabezado (-I agrega carpetas de inclusión; en Windows, las del SDK y las de MSVC se agregan automáticamente). --no-pp lo lee tal como está.
  • Las estructuras y uniones se convierten en tipos value con la disposición de C, las constantes de #define y de los enums se convierten en constantes, const char* se convierte en str, y los punteros a función se convierten en func*<…> : R.
  • Se omiten las funciones que la biblioteca no exporta, y también las que no se pueden llamar (argumentos variables, estructuras por valor).
  • --describe escribe lo que se leyó como un archivo .bind, que puedes editar y volver a usar con larva bind.

Una estructura del encabezado se usa por puntero, en código unsafe:

Antril
import System;
import Bindings;

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

Perfiles de bindings

Una biblioteca grande exige tomar decisiones: qué declaraciones conservar, cómo nombrarlas, dónde ponerlas. Un perfil de bindings es una clase de Antril que las toma. Deriva de System.Bindings.BindingProfile, y larva la compila y la ejecuta mientras genera el binding:

Terminal
larva bind --profile SDLBindingProfile.ant

Este es el perfil del paquete SDL3:

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;
	}
}

Con este perfil, la API de C de SDL se lee así desde 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;
}

Los hooks

larva llama a los métodos del perfil en este orden:

Método Cuándo Qué decide
Configure(BindingSettings s) Una vez, antes de leer los encabezados. Qué se lee y las reglas para todos los nombres.
Start(BindingModel m) Con todo ya leído. Enums creados a partir de grupos de constantes (m.MakeEnum("Keycode", "SDLK_*", false)), con sus typedefs.
Enum, Struct, Constant, Function Para cada declaración. false la omite. El hook también puede cambiar su nombre, sus tipos, su espacio de nombres, su clase o su archivo.
Name(NameKind kind, str cName, str proposed) Para cada nombre que ningún hook fijó. El nombre final.
Finish(BindingModel m) Con el binding completo. Los últimos cambios.

Si dos declaraciones terminan con el mismo nombre en el mismo lugar, la segunda conserva su nombre de C y larva muestra una advertencia.

La configuración

Propiedad Qué configura
Libraries, Headers, Includes Las bibliotecas nativas (se importa desde la primera), los encabezados y las carpetas de inclusión. Las bibliotecas y los encabezados indicados en la línea de comandos reemplazan a estos.
Namespace, Class, Assembly El espacio de nombres raíz (por defecto, Bindings), la clase de funciones y constantes (por defecto, el nombre de la biblioteca) y el nombre del ensamblado (por defecto, la clase más Bindings).
Output, Source La biblioteca que se escribe y la carpeta del código fuente de Antril (vacía: ninguno).
Split Cómo se divide el código fuente: SplitBy.None, Header, Namespace o Kind.
NamespaceByHeader, ClassByHeader, HeaderStrip Un espacio de nombres y una clase por encabezado, y prefijos que se quitan de los nombres de los encabezados.
Strip, StripTypes, StripFunctions, StripConstants Prefijos que se quitan de los nombres de C.
TypeCase, FunctionCase, ConstantCase, FieldCase, ParameterCase, MemberCase El estilo de mayúsculas de cada tipo de nombre: NameCase.Keep, Pascal, Camel, Upper, Snake.
StripMemberPrefix Los miembros de un enum pierden el prefijo que comparten.
EnumsFromC, AutoFlags Los enums de C se convierten en enums; los enums de bits distintos se convierten en enums de banderas de bits.
Exports Una función declarada que la biblioteca no exporta se omite (ExportCheck.Keep) o es un error (Check).
LongBits, AllHeaders El tamaño del long de C (32 en Windows, 64 en Linux y macOS); si también se conservan las declaraciones de los encabezados del sistema.

--dry-run ejecuta el perfil sin escribir nada, y --timings muestra cada paso. El editor analiza un perfil como cualquier archivo de Antril, con System.Bindings disponible.

Usar un binding en un proyecto

Un proyecto nombra el binding compilado con assembly: y la biblioteca nativa con lib: en su bloque DEPS. La biblioteca nativa se copia junto al programa. Deja el perfil y el código fuente generado fuera de la compilación:

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

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

Un binding nunca tiene el mismo nombre de archivo que la biblioteca nativa que envuelve: Windows cargaría una en lugar de la otra. Por eso larva bind lo nombra <Name>Bindings.dll o <Name>.Bindings.dll, y rechaza un -o que coincida con la biblioteca nativa.

Para compartir un binding, publícalo como un paquete de Hive.

¿Te resultó útil esta página?

No se guarda ninguna dirección; tu nombre de usuario sí, si iniciaste sesión.

Esc