Pular para o conteúdo

41 · FFI e nativo

1 exercícios: .

bash
python3 exercicios/run_all.py 41

Os exercícios#

#TítuloEnunciado
259chamar C: bibliotecas, ponteiros e callbacks

259 · chamar C: bibliotecas, ponteiros e callbacks#

exercicios/41-ffi-nativo/259_ffi_com_c.df
// A ponte para o Python resolve "preciso de algo que alguem escreveu em
// Python". Este e o degrau de baixo: chamar funcao de biblioteca C — a
// libm, a libz, o .so que a empresa mantem ha quinze anos — sem escrever
// modulo de extensao e sem trazer dependencia.

adopt Arcane.C as C
adopt Arcane.Bytes as B
adopt Arcane.Posse as P

// ── abrir e chamar, com a assinatura declarada ──
libm := C.matematica()
raiz := libm.funcao("sqrt", ["f64"], "f64")
potencia := libm.funcao("pow", ["f64", "f64"], "f64")

assert raiz(16.0) is 4.0
assert potencia(2.0, 10.0) is 1024.0

libc := C.padrao()
tamanho := libc.funcao("strlen", ["texto"], "tamanho")
absoluto := libc.funcao("abs", ["i32"], "i32")

assert tamanho("dataforge") is 9
assert absoluto(-42) is 42

// a biblioteca diz o que ela tem, antes de tentar
assert libc.tem("strlen")
assert not libc.tem("nao_existe_mesmo")

// declarar o tipo e o que impede o erro silencioso: um i32 onde o C
// espera i64 passa em quase toda chamada e corrompe memoria no resto
monitor:
    C.tamanho_de("inteirao")
    assert no
handle RuntimeError as e:
    assert "inteirao" in e.message

// os dois erros mais comuns de FFI dizem o nome e o caminho de saida
monitor:
    C.carregar("libnaoexisteaqui")
    assert no
handle RuntimeError as e:
    assert "libnaoexisteaqui" in e.message

monitor:
    libc.funcao("funcao_que_nao_existe", [], "i32")
    assert no
handle RuntimeError as e:
    assert "funcao_que_nao_existe" in e.message

// ── o layout de verdade: tamanho, alinhamento, deslocamento ──
assert C.tamanho_de("i8") is 1
assert C.tamanho_de("i32") is 4
assert C.tamanho_de("ponteiro") in [4, 8]
assert C.endianness() in ["little", "big"]

Ponto := C.estrutura([["x", "f64"], ["y", "f64"]])
assert Ponto.tamanho() is 16
assert Ponto.deslocamentos() is {"x": 0, "y": 8}

// o padding aparece: um i8 antes de um i32 nao ocupa 5 bytes
Mista := C.estrutura([["flag", "i8"], ["valor", "i32"]])
assert Mista.tamanho() is 8
assert Mista.deslocamentos()["valor"] is 4
assert Mista.alinhamento() is 4

Par := C.estrutura([["x", "i32"], ["y", "i32"]])
par := Par.criar({"x": 3, "y": 4})
assert par.ler("x") is 3
par.escrever("x", 30)
assert par.tudo() is {"x": 30, "y": 4}

// a uniao ocupa o maior campo
U := C.uniao([["i", "i32"], ["f", "f64"]])
assert U.tamanho() is C.tamanho_de("f64")

// um enum do C e inteiro com nome, e e o numero que atravessa
Cor := C.enumeracao({"VERMELHO": 0, "VERDE": 1})
assert Cor["VERDE"] is 1

// ── ponteiro cru: um endereco COM TIPO ──
bloco := C.alocar(4 * C.tamanho_de("i32"))
p := C.ponteiro(bloco, "i32")

cycle i from 0 to 3:
    p.deslocar(i).escrever(i * 10)

assert p.ler() is 0
assert p.deslocar(2).ler() is 20
assert [p.deslocar(i).ler() cycle i in range(0, 4)] is [0, 10, 20, 30]
C.liberar(bloco)

// o nulo e a unica conferencia que vale o custo: ler um endereco nulo
// derruba o processo, e a pilha nao fala do DataForge
nulo := C.nulo()
assert nulo.e_nulo() and nulo.endereco() is 0

monitor:
    nulo.ler()
    assert no
handle RuntimeError as e:
    assert "nulo" in e.message

// bytes vao e voltam
dados := C.de_bytes("dataforge")
assert B.para_texto(C.para_bytes(dados, 9)) is "dataforge"
assert dados.tamanho() is 10          // o zero do fim conta
C.liberar(dados)

// para nao depender de disciplina: o bloco sai com o escopo, inclusive
// quando o corpo falha
medido := P.com(P.dono(C.alocar(32), lambda b => C.liberar(b)),
                lambda b => b.tamanho())
assert medido is 32

// ── callback: o C chamando uma acao do DataForge ──
qsort := libc.funcao("qsort",
                     ["ponteiro", "tamanho", "tamanho", "ponteiro"], "void")

numeros := [42, 7, 19, 3]
area := C.alocar(len(numeros) * C.tamanho_de("i32"))
q := C.ponteiro(area, "i32")
cycle i in range(0, len(numeros)):
    q.deslocar(i).escrever(numeros[i])

action comparar(a, b):
    yield C.ponteiro(a, "i32").ler() - C.ponteiro(b, "i32").ler()

comparador := C.retorno_de_chamada(comparar, ["ponteiro", "ponteiro"], "i32")
qsort(area, 4, C.tamanho_de("i32"), comparador)

assert [q.deslocar(i).ler() cycle i in range(0, 4)] is [3, 7, 19, 42]
C.liberar(area)

// o tempo de vida e explicito: um callback coletado no meio de um qsort
// derruba o processo
action dobro(x):
    yield x * 2

cb := C.retorno_de_chamada(dobro, ["i32"], "i32")
assert cb.vivo()
assert cb.chamar(21) is 42
assert cb.endereco() bigger 0
cb.soltar()
assert not cb.vivo()

out "259 ok"

adopt Python.numpy resolve "preciso de uma biblioteca que alguém já escreveu em Python". Faltava o degrau de baixo: chamar uma função de uma biblioteca C — a libm, a libz, o .so que a empresa mantém há quinze anos — sem escrever módulo de extensão e sem trazer dependência. Arcane.C roda sobre o ctypes, que é da biblioteca padrão do Python: nada é instalado e nada é compilado.

A assinatura é declarada, e isso é o recurso#

dataforge
libm := C.matematica()
raiz := libm.funcao("sqrt", ["f64"], "f64")
assert raiz(16.0) is 4.0

Adivinhar o tipo erra fora do caso comum, e erra calado: um i32 onde o C espera i64 passa em quase toda chamada e corrompe memória no resto — longe da linha que causou. Declarar é o que impede isso.

A lista é fechada (i8i64, u8u64, f32, f64, bool, char, texto, bytes, ponteiro, tamanho, void), e um tipo inventado é recusado com a lista, em vez de virar um endereço qualquer.

Os dois erros que FFI sempre tem#

Biblioteca que não abre e símbolo que não existe. Os dois chegam do sistema ilegíveis (OSError: dlopen(…) no such file), e aqui os dois dizem o nome, onde foi procurado e o caminho de saída — por sistema operacional. lib.tem(nome) deixa perguntar antes de tentar.

O layout é a parte que ninguém acerta de cabeça#

dataforge
Mista := C.estrutura([["flag", "i8"], ["valor", "i32"]])
assert Mista.tamanho() is 8              // e nao 5
assert Mista.deslocamentos()["valor"] is 4

Tamanho, alinhamento e deslocamento vêm da ABI da plataforma. É o que quebra quando a struct do C muda de campo — e o que não dá para conferir lendo o cabeçalho de cabeça.

Ponteiro é um endereço COM TIPO#

O tipo não é decoração: é ele que diz quanto deslocar(1) anda e como ler() interpreta os bytes. deslocar_bytes(n) existe para quando o passo não é o tipo, e como(tipo) é o cast.

Só uma conferência é feita, e ela é a que vale o custo: ponteiro nulo. Ler um endereço nulo derruba o processo, e a pilha que sobra não fala do DataForge. O resto da segurança de memória, em FFI, é de quem chama — e o módulo não finge o contrário.

Para não depender de disciplina, junte com Arcane.Posse:

dataforge
P.com(P.dono(C.alocar(32), lambda b => C.liberar(b)),
      lambda bloco => bloco.tamanho())

O bloco sai no fim do escopo, inclusive quando o corpo falha.

O C chamando uma ação sua#

Metade das bibliotecas C úteis pede um ponteiro de função: qsort pede o comparador, a libcurl pede o recebedor, a libz pede o alocador.

dataforge
comparador := C.retorno_de_chamada(comparar, ["ponteiro", "ponteiro"], "i32")
qsort(area, 4, C.tamanho_de("i32"), comparador)

O qsort do C chama uma ação escrita em DataForge e ordena memória crua com o resultado.

O tempo de vida é explícito (vivo(), soltar()) por um motivo concreto: um callback coletado no meio de um qsort derruba o processo. Um objeto que se segura é o que impede isso, e vivo() é o que permite perguntar em vez de descobrir.

O que NÃO existe — e por quê#

  • binding automático a partir de `.h`: não há leitor de cabeçalho. A

assinatura é escrita à mão, e essa é a conferência.

  • C++ com nome decorado: o name mangling não é estável entre

compiladores. Só extern "C".

  • `stdcall` do Windows: só a convenção padrão.
  • bloco `unsafe`: não há bloco a marcar — o módulo inteiro é a

fronteira insegura, e a documentação diz isso em vez de espalhar uma palavra pelo código.


Rode um isolado com dataforge run exercicios/41-ffi-nativo/259_ffi_com_c.df.