O framework Foundation Models da Apple (macOS 26) expõe um LLM de ~3B parâmetros que roda on-device, de graça, sem necessidade de API key. Mas tem um problema: ele só trabalha com Swift. E não qualquer Swift — Swift async.

Seu tooling está em Python? Não tem binding. Em Rust? Também não. Um script de shell? Sem chance. O LLM mais barato do mundo (literalmente gratuito) está preso atrás de duas barreiras: o idioma e o modelo de concorrência.

A solução óbvia seria rodar um servidor HTTP local que expõe o modelo como uma API REST, tipo Ollama. Mas isso é como usar um canhão para matar uma mosca. Um processo rodando, uma porta ocupada, JSON indo e voltando, um curl para cada pergunta trivial. Para classificar um commit como fix ou feat, você não precisa de HTTP. Você precisa de uma função C.

A dylib de 4 funções

Por isso criei libfoundationmodels: uma biblioteca dinâmica que compila o framework da Apple em uma .dylib que exporta exatamente 4 funções C:

int32_t fm_init(void);
int32_t fm_is_available(void);
int32_t fm_generate(system, prompt, output, output_len);
int32_t fm_classify(system, prompt, choices, output, output_len);

Bem, 5 se você contar fm_generate_json, mas a ideia permanece a mesma. Quatro conceitos: inicializar, verificar disponibilidade, gerar texto livre e classificar forçando uma resposta entre opções. Tudo síncrono. Tudo bloqueante. Buffer de entrada, buffer de saída, código de retorno. C de sempre.

Usar isso a partir de Python é assim:

import ctypes

fm = ctypes.CDLL("libfoundationmodels.dylib")
fm.fm_init()

buf = ctypes.create_string_buffer(256)
fm.fm_classify(
    None,                                    # sem system prompt
    b"fix: handle nil response in OAuth",    # o texto para classificar
    b"fix\nfeat\nrefactor\ntest\ndocs",      # as opções
    buf, 256
)
print(buf.value.decode())  # "fix"

E pronto. Sem requests. Sem urllib. Sem JSON. Sem servidor rodando. Uma chamada a uma função C que, internamente, acorda o Neural Engine do seu Mac, passa o texto para ele e te devolve a resposta em um buffer. Latência típica: 200-800ms.

O problema interessante: async Swift em uma API síncrona C

Aqui começa a parte divertida. O framework Foundation Models é async:

let result = try await session.respond(to: prompt)

Esse await é o problema. C não sabe o que é await. C não tem structured concurrency. C tem funções que entram por cima, fazem algo e saem por baixo. Ponto final.

Você precisa de uma ponte que converta uma chamada async em uma chamada bloqueante. Ou seja: a thread de C precisa ficar parada esperando até que o async em Swift termine.

A solução clássica é usar um semáforo. Mas em Swift 6, com strict concurrency ativada, usar DispatchSemaphore dentro de código async é território perigoso. O compilador vai te dar um olhar zangado porque um semáforo pode bloquear uma thread do cooperative thread pool, exatamente o que Swift 6 tenta evitar.

Veja como resolvemos isso:

private func blockingCall<T: Sendable>(
    _ body: @Sendable @escaping () async -> T?
) -> T? {
    let box = Mutex<T?>(nil)
    let semaphore = DispatchSemaphore(value: 0)

    Task {
        let value = await body()
        box.withLock { $0 = value }
        semaphore.signal()
    }

    semaphore.wait()
    return box.withLock { $0 }
}

Há três peças trabalhando juntas:

  1. Mutex<T?> (do framework Synchronization de Swift, disponível desde macOS 15). É um lock que protege o valor de retorno. Por que não apenas uma simples var? Porque o Task escreve a partir de uma thread e o semaphore.wait() lê de outra. Sem o mutex, você teria um data race. Swift 6 acusaria isso imediatamente.

  2. DispatchSemaphore. O mecanismo de sinalização: a thread de C se bloqueia em .wait() e o Task faz .signal() quando termina. Aqui o truque é que o semáforo bloqueia a thread que chama (a de C), não uma thread do cooperative pool. O Task roda livremente em sua própria thread cooperativa.

  3. @_cdecl. O atributo que informa ao compilador Swift: “exporte esta função com name mangling de C”. Graças a isso, fm_generate aparece na tabela de símbolos da .dylib como uma função C normal, invocável a partir de qualquer linguagem.

A combinação é elegante porque cada peça resolve exatamente um problema: o mutex protege a memória compartilhada, o semáforo sincroniza as threads e @_cdecl expõe a interface. Não há mágica — apenas uma boa engenharia.

Por que funciona (e por que não é um hack)

O argumento contra usar DispatchSemaphore em código Swift moderno é válido: se você bloquear uma thread do cooperative thread pool, pode provocar um deadlock porque o pool tem um número fixo de threads. Se todas estiverem bloqueadas esperando semáforos, nenhuma executará os Task que fariam .signal().

Mas neste caso, o .wait() é feito pela thread de C — uma thread externa que não pertence ao cooperative pool. O Task executa no pool de Swift, faz seu trabalho async e sinaliza. Não há risco de starvation do pool porque a thread bloqueada não faz parte do pool.

É como um garçom (a thread de C) que pede um prato na cozinha (o Task async) e fica esperando no balcão. A cozinha tem seus próprios cozinheiros (o thread pool) e nunca se bloqueia por causa de um garçom esperando do lado de fora. O garçom não está ocupando um fogão.

@_cdecl: o atributo que ninguém documenta

Sobre @_cdecl. Repare no sublinhado: @_cdecl, não @cdecl. O sublinhado significa “atributo interno, não estável, pode mudar sem aviso”. Ele existe assim desde Swift 2 e é a forma padrão de facto de exportar funções Swift para C.

O Swift Evolution aprovou a proposta SE-0495 para formalizar @cdecl (sem sublinhado) como parte da linguagem. No Swift 6.2 já há suporte inicial, e no 6.3 ele será estabilizado junto com uma nova família de atributos @c para interop C/C++.

Significa que @_cdecl vai parar de funcionar? Não a curto prazo. Mas se você está construindo algo que pretende manter, migre para @cdecl assim que sua toolchain o suportar. O comportamento é o mesmo; só muda o nome.

Classificação forçada: o truque dos choices

A função mais útil da biblioteca não é fm_generate (texto livre), mas sim fm_classify:

fm_classify(
    "You classify git commit messages.",     // system prompt
    "fix: handle nil in OAuth refresh",      // texto para classificar
    "fix\nfeat\nrefactor\ntest\ndocs\nchore", // opções válidas
    buffer, sizeof(buffer)
);

Internamente, fm_classify faz algo que o framework da Apple não oferece diretamente a nível C: constrói um prompt que força o modelo a escolher uma das opções e valida a resposta:

let constrainedPrompt = """
    \(promptStr)

    You MUST reply with exactly one of these values, nothing else:
    \(choiceList.joined(separator: "\n"))
    """

// Depois de gerar, validar:
if choiceList.contains(where: {
    $0.caseInsensitiveCompare(raw) == .orderedSame
}) {
    return raw
}
// Fallback: buscar a opção dentro da resposta
return choiceList.first {
    raw.localizedCaseInsensitiveContains($0)
} ?? raw

Isso é constrained generation para pobres: você não usa o guided generation de @Generable (que exige definir um struct Swift), mas diz ao modelo “escolha uma destas” e depois verifica se ele fez isso. Se o modelo responder “The answer is fix” em vez de “fix”, o fallback detecta.

É tão robusto quanto @Generable? Não. Mas a partir de C você não pode definir um struct @Generable. E para classificação simples — que é o que cobre 80% dos casos de uso para tooling — funciona bem.

Smoke tests: 9 de 9

Os smoke tests são um arquivo C de 78 linhas que verifica:

  1. Que fm_is_available() retorna 0 ou 1 (não lixo)
  2. Que fm_init() retorna 0 se o modelo estiver disponível
  3. Que fm_classify retorna bytes positivos e um buffer não vazio
  4. Que a classificação produz uma resposta razoável
  5. Que fm_generate produz texto
  6. Que um buffer pequeno demais retorna -3 (truncado), sem crash
  7. Que fm_generate_json produz JSON válido

9 asserções, 9 passam. Em um Mac com Apple Intelligence ativado, make test leva cerca de 3 segundos. Se você não tem Apple Intelligence, os testes de geração são skipados e apenas verifica se fm_is_available() retorna 0. A biblioteca não faz crash em hardware sem suporte — apenas retorna -1.

Por que não um servidor HTTP?

Essa é a pergunta óbvia. Ollama, LM Studio, llama.cpp — todos expõem modelos como servidores HTTP locais. Por que uma dylib C é melhor?

Servidor HTTPdylib C
Latência~10-50ms overhead (TCP + JSON parse)~0 (chamada de função)
ProcessoPrecisa de um daemon rodandoCarregado sob demanda
DependênciasPorta livre, cliente HTTPUma linha de ctypes/extern "C"
IntegraçãoHTTP de qualquer linguagemFFI de qualquer linguagem
Overhead de memóriaProcesso separado (~50-200MB)Carregado no seu processo

Para um serviço que atende múltiplos clientes concorrentes, um servidor HTTP faz sentido. Mas para tooling de desenvolvimento — um pre-commit hook, um script de CI local, uma barra de menu — você não precisa de um servidor. Você precisa de uma função que você chama, obtém uma resposta e que desaparece.

É a diferença entre instalar PostgreSQL para guardar uma lista de compras e usar SQLite. Às vezes, a solução simples é a certa.

Como usar isso no seu idioma

A biblioteca produz um arquivo: libfoundationmodels.dylib. Um header: foundationmodels.h. Qualquer linguagem com suporte FFI para C pode utilizá-la:

  • Python: ctypes.CDLL("libfoundationmodels.dylib")
  • Rust: extern "C" { fn fm_classify(...) -> i32; }
  • Go: // #cgo LDFLAGS: -lfoundationmodels + import "C"
  • Ruby: FFI::Library com ffi_lib "foundationmodels"
  • Node.js: ffi-napi ou node-ffi

O padrão é idêntico em todos: carregar a dylib, declarar as assinaturas, chamar a função, ler o buffer. Se você sabe usar ctypes em Python ou extern "C" em Rust, já sabe usar isso.

O que complementa (e o que não substitui)

Esta biblioteca não substitui Ollama nem llama.cpp. Não roda modelos arbitrários, não suporta LoRA, não tem streaming. É um wrapper minimalista para o modelo que a Apple já te deu, para o caso de uso específico de tooling onde você quer classificação rápida, geração curta ou JSON estruturado sem montar infraestrutura.

Se o post anterior foi “seu Mac tem um LLM gratuito e você não está usando”, este é “e agora você pode usá-lo a partir de qualquer linguagem, não só Swift”. A camada 1 da arquitetura de modelos por camadas acabou de se abrir para todo o ecossistema.

Quatro funções C. Uma dylib. Sem servidor. Sem API key. Sem dependências. Às vezes, a melhor ferramenta é a que não precisa de manual de instruções.

Experimente

git clone https://github.com/frr149/libfoundationmodels
cd libfoundationmodels
make test       # 9 smoke tests (~3s)
make examples   # exemplo em C
python3 examples/classify.py  # exemplo em Python

Requisitos: Apple Silicon, macOS 26, Apple Intelligence ativado. Se você não tem macOS 26, os testes são skipados em vez de falhar.