FFI e baixo nível
Objetivo: chamar C com
@cimport, descer a assembly inline quando preciso, e entender por que a fronteira insegura é contida, não espalhada.
C entra por @cimport
Seção intitulada “C entra por @cimport”@cimport("header.h") roda em comptime, invoca um compilador C embutido, traduz o header inteiro
e devolve um módulo sob o namespace-raiz c. Você não redeclara protótipo nenhum:
@cimport("stdio.h") // → c.stdio@cimport("SDL2/SDL.h") as gfx // → c.gfx (renomeia o submódulo)
unsafe c.stdio.printf("%d\n", count) // tudo qualificado sob 'c.'Tudo do C fica sob c.: o ponto grita “isto é C” em todo contato, e seu namespace fica limpo (FFI
não polui quem não faz FFI). Os tipos fundamentais vêm de um runtime importado, porque são
dependentes de compilador/plataforma:
@cimport("gcc") // → c.gccsize: c.gcc.size_t = n // o size_t COMO O GCC o defineO NULL do C vira Optional[*c.gcc.char] na fronteira (capítulo 01: ausência é Optional); structs
suas indo pro C precisam de @repr(c) (layout fiel). Deps C são declaradas em cdeps no mk.project
(o .c compila junto).
Builds herméticos com musl embarcado
Seção intitulada “Builds herméticos com musl embarcado”A toolchain traz o próprio compilador C E a própria biblioteca C (musl), do jeito que o Zig faz, então um
build nunca depende de qual libc está instalada na máquina. --libc=musl linka seu programa estaticamente
contra essa musl embarcada, produzindo um binário que não depende de nada:
mk build --libc=musl server.mko // binário totalmente estático; o `ldd` diz "not a dynamic executable"O padrão (--libc=system) linka a biblioteca C do host dinamicamente, como de costume. A musl também é
importável como runtime, @cimport("musl") c.musl.*, carregando a ABI da própria musl do mesmo jeito que
c.gcc/c.clang carregam as suas; importá-la seleciona o build hermético automaticamente:
@cimport("musl")n: i32 = unsafe c.musl.abs(-7) assume "puro" // linka a musl embarcada, estático, sem libc do sistemamk cc: um compilador C/C++ drop-in
Seção intitulada “mk cc: um compilador C/C++ drop-in”A mesma toolchain embarcada é exposta direto como mk cc, um substituto de cc no espírito do zig cc. Ele
encaminha os argumentos ao compilador mas linka contra a musl embarcada, então compila C em qualquer máquina e
emite um binário estático, sem precisar da libc do sistema:
mk cc server.c -o server // um binário C totalmente estáticomk cc -c util.c -o util.o // compila uma unidade de tradução, drop-in como CC num MakefileIsso o torna usável como o CC de um projeto C existente, pra adoção incremental ao lado do Makoto. C++ também
compila (mk cc foo.cpp); linkar um programa C++ contra a biblioteca padrão de C++ precisa de um runtime C++
que a toolchain ainda não empacota.
Exportar Makoto pra C e C++
Seção intitulada “Exportar Makoto pra C e C++”A fronteira é de mão dupla. Uma função marcada @extern é exportada com seu nome C cru, então código C pode
chamá-la. mk build --emit=lib compila um arquivo Makoto (sem exigir main) numa biblioteca estática mais um
header C gerado:
@extern fn triple(x: i32) -> i32 { return x * 3 }mk build --emit=lib mathlib.mko // -> libmathlib.a + libmathlib.hO header declara cada export (envolto em extern "C", então C++ também chama) e makoto_init, que o chamador
roda uma vez no início pra armar o runtime do Makoto antes de chamar qualquer export que aloca:
#include "libmathlib.h"int main(void) { makoto_init(); return triple(14); } // linka libmathlib.a; retorna 42Como a convenção de chamada já é a do C (System V), um export é um símbolo C puro, sem shim. É assim que uma codebase C ou C++ adota Makoto incrementalmente: escreve um módulo em Makoto, linka como biblioteca, chama.
@cimport de um header C++ (.hpp/.hh/.hxx) parseia como C++: funções resolvem pros seus símbolos
Itanium-mangled, então o Makoto chama uma função livre C++ (em namespace ou não) e passa structs POD do mesmo
jeito que chama C:
@cimport("mathpp.hpp") // namespace mathpp { int add(int, int); }n := unsafe c.mathpp.add(20, 22) assume "c++"Um header cuja extensão não revela a linguagem (um .h que na verdade é C++, como algumas bibliotecas
distribuem) recebe um override explícito de linguagem como segundo argumento opcional, "c" ou "c++":
@cimport("legacy.h", "c++") // um .h que é realmente C++@cimport("weird.hpp", "c") // força C puro num .hppAs partes duras de C++ (templates, dispatch virtual, exceptions, RAII, a STL) são alcançadas do jeito que toda
linguagem as alcança: o compilador C++ é dono da semântica C++, e o Makoto chama um ponto de entrada C-ABI. Você
escreve um shim fino extern "C" em C++ e compila com mk cc; dentro dele você instancia templates, chama
métodos virtuais, faz try/catch e usa std:: à vontade:
// bridge.cpp — o mk cc compila isto; o compilador C++ cuida do C++.extern "C" int total(const int* xs, int n) { std::vector<int> v(xs, xs + n); // STL, RAII return std::accumulate(v.begin(), v.end(), 0);}O mk cc e o mk build --libc=musl linkam o runtime C++ automaticamente: a libstdc++ do host num build de
sistema, a libc++ embarcada (alvo musl) num hermético, então um programa Makoto que chama C++ ainda vira um
único binário totalmente estático. Uma função C++ sobrecarregada (dois símbolos, um nome) é um erro de
compilação na fronteira em vez de uma escolha silenciosa — dê à sobrecarga que você quer o seu próprio shim
extern "C".
Chamar C é unsafe
Seção intitulada “Chamar C é unsafe”Toda chamada a C é uma caixa-preta que pode violar qualquer invariante, então é uma operação
unsafe, abatida com assert/assume ou agrupada num bloco:
n := unsafe c.unistd.read(fd, buf, len) assume "fd válido e buf comporta len bytes"
unsafe { c.SDL.SDL_Init(c.SDL.SDL_INIT_VIDEO) win := c.SDL.SDL_CreateWindow(...)}E o retorno do C valida na borda: ponteiro que pode ser NULL vira Optional, bytes que viram
string passam por Result. Depois de validado, você opera com as garantias normais.
unsafe é contível
Seção intitulada “unsafe é contível”unsafe não é uma região onde tudo é permitido e silencioso (modelo Rust). É uma obrigação de
tratamento: cada operação perigosa exige um lidador (assert/assume) ou é repassada (a função
vira unsafe fn). Qualquer função que cumpra as pré-condições absorve o perigo e expõe interface
segura por cima. Por isso unsafe vive em ilhas pequenas com fronteiras seguras, e não num call
stack inteiro pintado.
Inline assembly
Seção intitulada “Inline assembly”A extensão mais baixa da FFI: instruções de máquina cruas, para um syscall, um rdtsc, uma SIMD que
o compilador não emite. O binding de registrador é via @asm:
@asm(x86, intel)unsafe fn add10(value: u32 @asm(in, rax)) -> u32 @asm(out, rbx) { mov rbx, rax add rbx, 10}Seleção por arquitetura é comptime (via o pacote arch); mismatch de arch é erro de compilação (não
há fallback mágico, porque um mov rax não existe em ARM).
use archfn timestamp() -> u64 { comptime if arch.current == .x86_64 { return unsafe rdtsc_x86() assume "rdtsc disponível neste alvo" } return portable_clock()}Próximo: 14 · Extensão de UI