Interoperabilidade: chamando C a partir de outras linguagens
1. Fundamentos da Interoperabilidade com C
1.1. A ABI (Application Binary Interface) do C
A interoperabilidade bem-sucedida depende do entendimento da ABI do C. Diferente de uma API (definida em código fonte), a ABI define como funções são chamadas em nível binário: convenção de chamada (cdecl, stdcall, fastcall), alinhamento de structs, tamanho de tipos primitivos e layout de memória. O padrão C não especifica uma ABI única — cada plataforma (x86, ARM, RISC-V) e sistema operacional define sua própria ABI. Por isso, ao exportar funções C, você deve garantir que o chamador use a mesma ABI.
1.2. O papel dos cabeçalhos .h e bibliotecas compartilhadas
Arquivos de cabeçalho (.h) fornecem as declarações que outras linguagens podem interpretar para gerar bindings. Bibliotecas compartilhadas (.so no Linux, .dll no Windows, .dylib no macOS) contêm o código binário real. A interoperabilidade exige que ambos estejam sincronizados: o cabeçalho descreve a interface, a biblioteca fornece a implementação.
1.3. Name mangling: por que C não decora nomes
C não realiza name mangling (decoração de nomes). Uma função int soma(int a, int b) no código objeto aparece exatamente como soma. Isso contrasta com C++, que decora nomes para suportar sobrecarga. Essa simplicidade do C é vantajosa para interoperabilidade: outras linguagens podem chamar soma diretamente, sem precisar lidar com símbolos decorados.
2. A Interface C: extern "C" e Exportação de Símbolos
2.1. Uso de extern "C" em C++
Ao compilar uma biblioteca em C++ que precisa ser chamada por outras linguagens, envolva as funções com extern "C" para evitar name mangling:
#ifdef __cplusplus
extern "C" {
#endif
int soma(int a, int b);
#ifdef __cplusplus
}
#endif
2.2. Atributos de visibilidade
No Windows, use __declspec(dllexport) para exportar símbolos:
__declspec(dllexport) int soma(int a, int b);
No GCC/Clang (Linux/macOS), use __attribute__((visibility("default"))):
__attribute__((visibility("default"))) int soma(int a, int b);
Ou compile com -fvisibility=hidden e torne visível apenas o que desejar.
2.3. Criação de uma API C limpa
Uma API C ideal para interoperabilidade deve:
- Usar funções planas (sem sobrecarga)
- Evitar templates ou polimorfismo
- Usar tipos opacos (typedef struct MinhaStruct MinhaStruct;) para encapsular dados
- Fornecer funções create/destroy para gerenciamento de memória
3. Chamando C a partir de Python (ctypes e cffi)
3.1. Exemplo com ctypes
Suponha a biblioteca libmatematica.so com a função:
int soma(int a, int b);
Em Python:
import ctypes
lib = ctypes.CDLL("./libmatematica.so")
lib.soma.argtypes = [ctypes.c_int, ctypes.c_int]
lib.soma.restype = ctypes.c_int
resultado = lib.soma(3, 4)
print(resultado) # 7
3.2. Exemplo com cffi
cffi permite gerar bindings diretamente de cabeçalhos C:
from cffi import FFI
ffi = FFI()
ffi.cdef("int soma(int a, int b);")
lib = ffi.dlopen("./libmatematica.so")
resultado = lib.soma(3, 4)
3.3. Tratamento de strings e ponteiros
Para funções que retornam strings, use ctypes.c_char_p e gerencie a memória com create_string_buffer:
lib.mensagem.restype = ctypes.c_char_p
msg = lib.mensagem()
print(msg.decode('utf-8'))
4. Chamando C a partir de Java (JNI)
4.1. Estrutura de um arquivo JNI
A função nativa Java public native int soma(int a, int b); gera o cabeçalho JNI:
JNIEXPORT jint JNICALL Java_Main_soma(JNIEnv *env, jobject obj, jint a, jint b) {
return a + b;
}
4.2. Mapeamento de tipos
| Tipo Java | Tipo JNI | Tipo C |
|---|---|---|
int |
jint |
int32_t |
String |
jstring |
const char* (após conversão) |
double[] |
jdoubleArray |
Ponteiro para double |
Para converter jstring para C:
const char *c_str = (*env)->GetStringUTFChars(env, jstr, NULL);
// usar c_str
(*env)->ReleaseStringUTFChars(env, jstr, c_str);
4.3. Gerenciamento de memória
Use NewGlobalRef para manter referências entre chamadas e DeleteLocalRef para evitar vazamentos. Referências locais são automaticamente liberadas ao retornar da função nativa, mas em loops longos é essencial limpá-las manualmente.
5. Chamando C a partir de C# / .NET (P/Invoke)
5.1. Declaração de DllImport
[DllImport("libmatematica.so", CallingConvention = CallingConvention.Cdecl)]
public static extern int soma(int a, int b);
5.2. Ponteiros, structs e callbacks
Para structs, use MarshalAs e StructLayout:
[StructLayout(LayoutKind.Sequential)]
public struct Ponto {
public int x;
public int y;
}
[DllImport("libgeometria.so")]
public static extern double distancia(ref Ponto p1, ref Ponto p2);
Callbacks exigem Marshal.GetFunctionPointerForDelegate:
public delegate void Callback(int valor);
[DllImport("libalgo.so")]
public static extern void registrar_callback(IntPtr callbackPtr);
5.3. Diferenças entre plataformas
No Windows, use .dll; no Linux, .so; no macOS, .dylib. O .NET Core/5+ detecta automaticamente com base no runtime. Use constantes de plataforma ou carregamento condicional.
6. Chamando C a partir de Node.js (Node-API / N-API)
6.1. Estrutura de um addon nativo
Com N-API puro (C):
#include <node_api.h>
napi_value Soma(napi_env env, napi_callback_info info) {
napi_value args[2];
size_t argc = 2;
napi_get_cb_info(env, info, &argc, args, NULL, NULL);
int a, b;
napi_get_value_int32(env, args[0], &a);
napi_get_value_int32(env, args[1], &b);
napi_value result;
napi_create_int32(env, a + b, &result);
return result;
}
napi_value Init(napi_env env, napi_value exports) {
napi_value fn;
napi_create_function(env, NULL, 0, Soma, NULL, &fn);
napi_set_named_property(env, exports, "soma", fn);
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
6.2. Manipulação de valores JavaScript
Use funções como napi_create_string_utf8, napi_get_array_length, napi_get_element para manipular arrays e strings.
6.3. Ciclo de vida
O módulo é inicializado uma vez. Exporte funções que serão chamadas do JavaScript. O garbage collection do Node.js gerencia objetos JavaScript, mas recursos C alocados precisam ser liberados manualmente.
7. Chamando C a partir de Rust (FFI)
7.1. Declaração de funções externas
extern "C" {
fn soma(a: i32, b: i32) -> i32;
}
fn main() {
unsafe {
println!("{}", soma(3, 4)); // 7
}
}
7.2. Tipos equivalentes
Use o crate libc para tipos portáveis: c_int, c_char, c_void. Mapeie int32_t para i32, uint64_t para u64.
7.3. Segurança
Blocos unsafe são obrigatórios para chamadas FFI. Ponteiros brutos (*const T, *mut T) podem causar undefined behavior se mal utilizados. Siga as regras de ownership do Rust mesmo no FFI: não libere memória alocada pelo C com alocadores Rust.
8. Boas Práticas para uma API C Interoperável
8.1. Evitar dependências de alocadores internos
Forneça funções create/destroy explícitas que usam o mesmo alocador:
MinhaStruct* minha_struct_create();
void minha_struct_destroy(MinhaStruct* s);
8.2. Uso de tipos de tamanho fixo
Use int32_t, uint64_t de <stdint.h> em vez de int ou long, que variam entre plataformas.
8.3. Documentação da ABI
Documente:
- Versão da biblioteca
- Convenção de chamada (cdecl, stdcall)
- Alinhamento esperado de structs
- Garantias de thread-safety
- Como compilar para cada plataforma alvo
Referências
- Documentação oficial ctypes (Python) — Guia completo para chamar bibliotecas C a partir de Python com ctypes.
- Java Native Interface Specification — Documentação oficial da Oracle sobre JNI, incluindo mapeamento de tipos e gerenciamento de memória.
- P/Invoke no .NET (Microsoft Docs) — Tutorial completo sobre Platform Invoke para chamar código nativo de C#.
- Node-API (N-API) Documentation — Documentação oficial da API nativa do Node.js para criar addons em C/C++.
- The Rust FFI Omnibus — Guia do Rust sobre FFI, incluindo tipos equivalentes, segurança e boas práticas.
- C Interoperability Guide (GCC) — Documentação sobre atributos de visibilidade e convenções de chamada no GCC.
- ABI for the ARM Architecture — Especificação da ABI ARM, relevante para interoperabilidade em dispositivos embarcados.