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