Skip to content

Repository files navigation

ffi-converter

Generate Go bindings for C libraries without CGo.

What This Does

ffi-converter reads a C header file and generates a Go package that calls the native library at runtime through libffi. This means:

  • No CGo required — your code compiles with go build, no C compiler needed
  • Cross-compilation works — build for any platform from any platform
  • Faster builds — no CGo compilation overhead

Quick Start

# Build the tool
go build .

# Generate bindings from a C header
./ffi-converter -header mylib.h -output ./mylib -package mylib -lib mylib
package main

import "myproject/mylib"

func main() {
    if err := mylib.Load("/path/to/libs"); err != nil {
        panic(err)
    }

    result := mylib.SomeFunction(42)
}

Load is safe for concurrent use and idempotent: a second successful call is a no-op rather than a reload that leaks the first library handle.

Command-Line Options

Flag Required Description
-header Yes Path to the C header file
-output No Output directory (default: current directory)
-package No Go package name (default: bindings)
-lib No Library name; mylib becomes libmylib.so / libmylib.dylib / mylib.dll (default: derived from the header filename)

Diagnose and Skip — the central contract

Anything the tool cannot represent faithfully in Go is skipped whole and reported, never half-generated. Every skip is printed to stderr as a warning: line naming the construct and the reason:

warning: line 1: skipped union "U": unions are not supported
warning: line 2: skipped struct "S": function pointer or parenthesised declarator is not supported
warning: line 4: skipped struct "M": multi-dimensional arrays are not supported
warning: skipped function "ld_fn": return type: C type "long double" has no Go representation
warning: skipped function "printf_like": variadic functions need one prepared call interface per call site
warning: C long is mapped to Go int64: correct on LP64 targets, wrong on Windows LLP64 and on 32-bit targets

Warnings do not fail the run; the tool still exits 0 and writes the bindings for everything it could translate. Read them. A missing wrapper is always explained by a warning — if a function you expected is absent from the output, the reason is on stderr. The tool exits non-zero only when the header cannot be read or the generated code cannot be produced at all.

What Gets Generated

Given testdata/calculator.h:

typedef struct {
    double value;
    int32_t precision;
    uint8_t use_cache;
} CalcConfig;

typedef struct Calc_s* Calc;

CalcConfig calc_default_config(void);
Calc calc_create(CalcConfig config);
void calc_free(Calc calc);
double calc_add(Calc calc, double a, double b);
const char* calc_get_version(void);
int32_t calc_format(Calc calc, char* buf, size_t buf_size);

the tool writes three files (see the committed sample in testdata/out/):

loader.go

Opens the shared library with platform detection (.so, .dylib, .dll) and prepares one libffi call interface per wrapped function.

types.go

Go structs matching the C structs, plus the matching libffi layout descriptors:

type CalcConfig struct {
    Value     float64
    Precision int32
    UseCache  uint8
}

var FFITypeCalcConfig = ffi.NewType(
    &ffi.TypeDouble,
    &ffi.TypeSint32,
    &ffi.TypeUint8,
)

type Calc uintptr // opaque handle

functions.go

Go functions that call into the native library:

func CalcDefaultConfig() CalcConfig
func CalcCreate(config CalcConfig) Calc
func CalcFree(calc Calc)
func CalcAdd(calc Calc, a float64, b float64) float64
func CalcGetVersion() string
func CalcFormat(calc Calc, buf []byte, bufSize uint) int32

Note the string handling: const char* becomes a Go string, while a writable char* output buffer becomes []byte.

Supported C Features

  • Primitive types: int, char, short, long, long long, float, double, bool
  • Fixed-width and platform types: int32_t, uint64_t, size_t, ssize_t, uintptr_t, ptrdiff_t
  • Typedefs, including chains, resolved to the type they alias
  • Structs passed by value or by pointer, including one-dimensional array members
  • Opaque handles (typedef struct X_s* X)
  • Enums, with C value semantics ({ RED = 1, GREEN, BLUE } is 1, 2, 3) and an underlying Go integer type wide enough for every member
  • Strings: const char* parameters and char* returns as string, writable char* / unsigned char* buffers as []byte
  • Pointer parameters, void*, and struct pointers
  • Out-parameters: char** becomes **byte, any other T** or deeper becomes *unsafe.Pointer — real Go pointers, so the call is safe for the garbage collector to move memory around
  • extern "C" { ... } blocks, including the usual #ifdef __cplusplus wrapper that most C libraries put around their entire API
  • Array parameters, which decay to a pointer as they do in C: given typedef unsigned char buf_t[16];, the typedef emits Go type BufT [16]uint8 while void take(buf_t b) becomes func Take(b []byte)
  • Duplicate declarations: identical redeclarations (what a broken include guard produces) collapse to one wrapper; a genuinely conflicting one keeps the first and warns

Not Supported (skipped with a diagnostic)

Each of these produces a warning: line and the enclosing declaration is dropped rather than mis-translated:

  • Unions
  • Function-pointer struct members, and functions returning function pointers
  • Anonymous nested structs and unions inside a struct
  • Bitfields
  • Multi-dimensional and non-constant-sized arrays
  • long double — Go has no extended-precision float
  • Variadic functions — libffi needs one prepared call interface per call site
  • Function definitions in the header (static inline ...); only declarations are translated
  • File-scope variable declarations
  • Callbacks, which still require manual setup with ffi.Closure
  • Preprocessor macros beyond being tolerated in a declaration; the tool does not run a preprocessor and does not follow #include. Headers that build their declarations out of macros are the main casualty — zlib's int foo OF((int a)); idiom yields nothing, because only a preprocessor can tell what OF expands to.

long / unsigned long are supported but emit a portability warning: they are mapped to 64-bit Go types, which is correct on LP64 targets and wrong on Windows LLP64 and on 32-bit targets. size_t and friends have no such problem — their libffi type is chosen at init from unsafe.Sizeof.

Requirements

Build Time

Go 1.25.6 or newer (see go.mod). The tool itself has no dependencies outside the standard library.

Runtime

  • Linux/FreeBSD: install libffi (apt install libffi8, dnf install libffi)
  • macOS: libffi is bundled
  • Windows (AMD64): libffi is bundled

Generated Code Dependencies

Add these to the go.mod of the project that consumes the bindings:

require (
    github.com/jupiterrider/ffi v0.7.0
    golang.org/x/sys v0.28.0
)

Do not downgrade github.com/jupiterrider/ffi below v0.5.1. Earlier releases mirrored ffi_cif as a single 32-byte struct on every architecture, but the aarch64 ABI defines FFI_EXTRA_CIF_FIELDS, making the real ffi_cif 40 bytes. On arm64 ffi_prep_cif therefore wrote 8 bytes past the end of the Go allocation, and once the neighbouring object was reused every later call marshalled from corrupted state — float and double arguments arriving as zero, plus intermittent SIGSEGVs. v0.7.0 is the pinned, verified version.

How It Works

  1. Lex and parse — a hand-written tokenizer (parser/lex.go) feeds a declarator parser (parser/decl.go); there is no regex matching of C syntax. extern "C" blocks are descended into, so a real-world header works: the system sqlite3.h yields 117 wrappers. Anything unrepresentable is recorded in Header.Diagnostics instead of being guessed at.
  2. Resolve — typedef chains are followed and each C type is looked up in a single table that gives its Go type, its libffi type and its width.
  3. Plan, then emit — the generator decides every name, signature and call shape up front, then writes the Go source and runs it through go/format, so a generator bug becomes a hard error rather than a broken output file.

The generated code uses jupiterrider/ffi, which wraps libffi for Go.

For the internals — the type table, the FFI calling patterns, and how to add support for a new C type — see AGENTS.md.

License

Apache 2.0

About

This project takes a C library and creates the Go FFI bindings for it

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages