Documentation
¶
Overview ¶
Package ffi provides a Foreign Function Interface for calling C functions from Go without CGO. It enables direct calls to C libraries with full type safety and platform abstraction.
Overview ¶
This package allows you to:
- Load dynamic libraries (LoadLibrary)
- Get function pointers (GetSymbol)
- Prepare function call interfaces (PrepareCallInterface)
- Execute C function calls (CallFunction)
Basic Usage ¶
// Load a library
handle, err := ffi.LoadLibrary("libm.so.6")
if err != nil {
log.Fatal(err)
}
// Get a function pointer
sqrtPtr, err := ffi.GetSymbol(handle, "sqrt")
if err != nil {
log.Fatal(err)
}
// Prepare call interface
var cif types.CallInterface
err = ffi.PrepareCallInterface(
&cif,
types.DefaultCall,
types.DoubleTypeDescriptor,
[]*types.TypeDescriptor{types.DoubleTypeDescriptor},
)
// Call the function (the first result is the C errno after the call)
var result float64
arg := 16.0
_, err = ffi.CallFunction(
&cif,
sqrtPtr,
unsafe.Pointer(&result),
[]unsafe.Pointer{unsafe.Pointer(&arg)},
)
// result is now 4.0
Supported Platforms ¶
- Linux, Windows, macOS, FreeBSD (AMD64 + ARM64) — 8 production targets
- Android ARM64 (API 29+, guarded preview)
Performance ¶
Hand-written assembly for System V AMD64 and AAPCS64; Windows AMD64 calls go through syscall.SyscallN. Overhead: 88-114 ns/op with errno capture. sync.Pool for callback stack-move safety: the call path allocates nothing in steady state. The avalue slice escapes, so reuse it instead of building a slice literal on every call.
Safety ¶
While this package uses unsafe.Pointer internally, the public API validates all inputs and provides type-safe wrappers. Users should ensure:
- Argument types match the C function signature exactly
- Pointers remain valid during the call (use runtime.KeepAlive if needed)
- Pointer arguments are held in unsafe.Pointer variables, never uintptr (a uintptr hides a stack variable from escape analysis; see CallFunctionContext)
- Go pointers stored in memory that C reads (structs, callback frames) are pinned with runtime.Pinner
- Return value buffer is large enough for the result
Thread Safety ¶
This package follows Go's standard library conventions for concurrent access:
- PrepareCallInterface and CallFunction are safe to call concurrently with different CallInterface instances
- A prepared CallInterface is only read by CallFunction, so many goroutines may call through it at once, each with its own argument values and result buffer
- DO NOT prepare (or otherwise modify) a CallInterface while calls through it are in flight
- PrepareCallInterface fills in Size and Alignment of struct descriptors whose Size is 0; prepare once before sharing such a descriptor between goroutines
- Library handles (from LoadLibrary) are safe to use concurrently for read operations (GetSymbol)
- DO NOT call FreeLibrary while other goroutines are using GetSymbol on the same handle
- Similar to io.Reader: methods are not inherently thread-safe; synchronization is caller's responsibility
The race detector needs cgo, so it is available only with CGO_ENABLED=1, where goffi uses the real runtime/cgo instead of internal/fakecgo; CI runs the test suite with -race. The library's own shared state (Registry, TypeDescriptors) is initialized once at startup and accessed read-only thereafter.
Zero Dependencies ¶
This package has zero external module dependencies. With CGO_ENABLED=0 the cgo runtime comes from goffi's own internal/fakecgo (Unix). All FFI logic is implemented in pure Go and assembly.
Index ¶
- Constants
- Variables
- func CallFunction(cif *types.CallInterface, fn unsafe.Pointer, rvalue unsafe.Pointer, ...) (syscall.Errno, error)
- func CallFunctionContext(ctx context.Context, cif *types.CallInterface, fn unsafe.Pointer, ...) (syscall.Errno, error)
- func FreeLibrary(handle unsafe.Pointer) error
- func GetSymbol(handle unsafe.Pointer, name string) (unsafe.Pointer, error)
- func LoadLibrary(name string) (unsafe.Pointer, error)
- func NewCallback(fn any) uintptr
- func PrepareCallInterface(cif *types.CallInterface, convention types.CallingConvention, ...) error
- func PrepareVariadicCallInterface(cif *types.CallInterface, convention types.CallingConvention, nfixedargs int, ...) error
- type CallingConventionError
- type InvalidCallInterfaceError
- type LibraryError
- type TypeValidationError
- type UnsupportedPlatformError
Constants ¶
const ( // RTLD_NOW resolves all symbols when loading the library (recommended). RTLD_NOW = dl.RTLD_NOW // RTLD_GLOBAL makes symbols available for subsequently loaded libraries. RTLD_GLOBAL = dl.RTLD_GLOBAL )
RTLD constants from <dlfcn.h> for dynamic library loading.
Variables ¶
var ( // ErrInvalidCallInterface is deprecated. Use InvalidCallInterfaceError instead. // // Deprecated: This sentinel error is kept for backwards compatibility only. // New code should use errors.As() with *InvalidCallInterfaceError to get // detailed error information. ErrInvalidCallInterface = &InvalidCallInterfaceError{ Field: "unknown", Reason: "invalid call interface", Index: -1, } // ErrFunctionCallFailed is deprecated. Use specific typed errors instead. // // Deprecated: This generic error doesn't provide useful debugging information. // New code should handle specific error types returned by CallFunction. ErrFunctionCallFailed = fmt.Errorf("function call failed") )
Deprecated: Legacy sentinel errors kept for backwards compatibility. Use typed errors above with errors.As() for better error handling.
var ErrStaticBuild = errors.New("goffi: dynamic loading unavailable in static build (-tags goffi_static)")
ErrStaticBuild is returned by LoadLibrary / GetSymbol / FreeLibrary when the binary was built with -tags goffi_static. That profile removes all //go:cgo_import_dynamic directives so the Go internal linker can emit a fully static ELF; dynamic library loading is intentionally unavailable.
Use errors.Is(err, ErrStaticBuild) or unwrap through *LibraryError.
var ErrTooManyArguments = errors.New("goffi: argument count exceeds platform limit")
ErrTooManyArguments is returned when the argument count exceeds the platform limit of registers plus stack slots supported by the syscall layer.
Functions ¶
func CallFunction ¶
func CallFunction( cif *types.CallInterface, fn unsafe.Pointer, rvalue unsafe.Pointer, avalue []unsafe.Pointer, ) (syscall.Errno, error)
CallFunction executes a C function call without context support.
This is equivalent to CallFunctionContext(context.Background(), cif, fn, rvalue, avalue). For operations that need cancellation or timeout control, use CallFunctionContext instead.
errno is captured inside the assembly trampoline immediately after the C function returns, before the Go runtime can migrate the goroutine to a different OS thread. On Windows, errno is always 0.
Parameters:
- cif: Prepared call interface (from PrepareCallInterface)
- fn: Function pointer obtained from GetSymbol (must not be nil)
- rvalue: Pointer to buffer for return value (can be nil for void functions)
- avalue: Slice of pointers to argument values, one per argument type of cif
Returns:
- errno: C errno captured after the call (0 on success or Windows)
- err: nil on success; *InvalidCallInterfaceError if cif, fn or cif.ReturnType is nil, len(avalue) differs from the number of argument types, or a struct result of a CallInterface not filled by PrepareCallInterface has no Size or Alignment
Example:
// Calling open(2) and checking errno on failure:
errno, err := ffi.CallFunction(
&cif,
openFn,
unsafe.Pointer(&result),
[]unsafe.Pointer{unsafe.Pointer(&pathPtr), unsafe.Pointer(&flags)},
)
if result == -1 {
log.Printf("open failed: %v", errno)
}
// When errno is not needed:
_, err := ffi.CallFunction(&cif, strlenFn, unsafe.Pointer(&result), avalue)
For context-aware calls with timeout support, see CallFunctionContext.
func CallFunctionContext ¶
func CallFunctionContext( ctx context.Context, cif *types.CallInterface, fn unsafe.Pointer, rvalue unsafe.Pointer, avalue []unsafe.Pointer, ) (syscall.Errno, error)
CallFunctionContext executes a C function call with context support.
This function performs the actual FFI call to the C function, handling all platform-specific calling convention details automatically. It checks the context before executing to prevent starting expensive operations when the context is already cancelled or has exceeded its deadline.
errno is captured inside the assembly trampoline immediately after the C function returns, before the Go runtime can migrate the goroutine to a different OS thread. On Windows, errno is always 0 (use syscall.GetLastError() for Win32 error codes).
Parameters:
- ctx: Context for cancellation and timeout control (use context.Background() if not needed)
- cif: Prepared call interface (from PrepareCallInterface)
- fn: Function pointer obtained from GetSymbol (must not be nil)
- rvalue: Pointer to buffer for return value (can be nil for void functions)
- avalue: Slice of pointers to argument values, one per argument type of cif
Returns:
- errno: C errno captured after the call (0 on success or Windows)
- err: nil on success; ctx.Err() if context cancelled; *InvalidCallInterfaceError if cif, fn or cif.ReturnType is nil, len(avalue) differs from the number of argument types, or a struct result of a CallInterface not filled by PrepareCallInterface has no Size or Alignment
Example:
// Call with timeout
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
var result float64
arg := 16.0
errno, err := ffi.CallFunctionContext(
ctx,
&cif,
sqrtPtr,
unsafe.Pointer(&result),
[]unsafe.Pointer{unsafe.Pointer(&arg)},
)
if err == context.DeadlineExceeded {
log.Println("Call timed out")
}
Note:
- Context cancellation check occurs BEFORE the call to prevent starting expensive operations when the context is already cancelled.
- Once the C function starts executing, it CANNOT be interrupted mid-flight.
- For cancellable operations, the C library itself must support cancellation.
avalue Convention (critical for correctness) ¶
Each avalue[i] is a pointer TO the argument value, following the libffi convention. GoFFI dereferences avalue[i] to read the value passed to the C function:
Input scalar (int x): avalue[i] = unsafe.Pointer(&x)
Input pointer (char *buf): avalue[i] = unsafe.Pointer(&buf) // GoFFI reads buf → C gets buf
Out-pointer (int *result): avalue[i] = unsafe.Pointer(&resultPtr)
where resultPtr = unsafe.Pointer(&result)
The out-pointer case requires an intermediate variable. Without it, GoFFI reads the VALUE of result (likely 0) instead of its ADDRESS, and the C function receives NULL:
// WRONG — C receives NULL:
var handle unsafe.Pointer
avalue := []unsafe.Pointer{unsafe.Pointer(&handle)} // GoFFI reads handle (nil)
// CORRECT — C receives &handle:
var handle unsafe.Pointer
handlePtr := unsafe.Pointer(&handle)
avalue := []unsafe.Pointer{unsafe.Pointer(&handlePtr)} // GoFFI reads handlePtr (&handle)
Safety:
- All argument pointers must remain valid during the call
- Hold pointer arguments in unsafe.Pointer (or *T) variables, never uintptr, all the way from where the address is taken to avalue. That includes the parameters of your own binding wrappers: a wrapper such as func MapMemory(..., ppData uintptr) loses the pointer even when the caller writes uintptr(unsafe.Pointer(&local)) in its argument list. The compiler keeps a uintptr's target alive and unmoved only in the argument list of a call to a function implemented in assembly or marked //go:uintptrescapes (package unsafe, rule (4)); CallFunction and your wrapper are ordinary Go functions. A local reachable only through a uintptr stays on the goroutine stack, the stack can move before the C function runs, and C then reads or writes the old address.
- Memory reachable from rvalue and avalue through pointer-typed values is kept alive and in place for the call: CallFunction lets avalue escape, so such memory is allocated on the heap, which the GC does not move.
- If C keeps a pointer to Go memory after the call returns, or reads it from a uintptr-typed C struct field or an out-parameter slot that must stay uintptr, pin it with runtime.Pinner for as long as C uses it, or use memory allocated by C
- Return value buffer must be large enough for the result type
- Use runtime.KeepAlive() if needed to prevent premature GC of arguments
func FreeLibrary ¶
FreeLibrary unloads a previously loaded library using dlclose.
This function decrements the reference count of the loaded library. When the reference count reaches zero, the library is unloaded from memory.
Parameters:
- handle: Library handle from LoadLibrary (can be nil)
Returns:
- nil on success
- Error if the library could not be unloaded
Example:
handle, err := ffi.LoadLibrary("libm.so.6")
if err != nil {
log.Fatal(err)
}
defer ffi.FreeLibrary(handle)
Safety:
- Do not use function pointers obtained from this library after FreeLibrary
- Always pair LoadLibrary with FreeLibrary to prevent resource leaks
- Safe to call with nil handle (returns nil without error)
func GetSymbol ¶
GetSymbol retrieves a function pointer from a loaded library using dlsym.
This function looks up a symbol (function or variable) in the loaded library and returns its address for use with CallFunction.
Parameters:
- handle: Library handle from LoadLibrary
- name: Name of the symbol to retrieve (e.g., "sqrt", "glClear")
Returns:
- Function pointer (use with CallFunction)
- Error if symbol not found or lookup fails
Example:
sqrtPtr, err := ffi.GetSymbol(handle, "sqrt")
if err != nil {
log.Fatal(err)
}
Note: The returned pointer is only valid while the library remains loaded.
func LoadLibrary ¶
LoadLibrary loads a shared library using dlopen.
This function loads the specified shared library and returns a handle for use with GetSymbol. The library is loaded with RTLD_NOW|RTLD_GLOBAL flags.
Parameters:
- name: Path to the shared library (e.g., "libm.so.6", "/usr/lib/libGL.so.1")
Returns:
- Handle to the loaded library (use with GetSymbol and FreeLibrary)
- Error if loading fails
Example:
handle, err := ffi.LoadLibrary("libm.so.6")
if err != nil {
log.Fatal(err)
}
defer ffi.FreeLibrary(handle)
Note: Always pair LoadLibrary with FreeLibrary to prevent resource leaks.
func NewCallback ¶ added in v0.2.0
NewCallback registers a Go function as a C callback and returns a function pointer. The returned uintptr can be passed to C code as a callback function pointer.
Requirements:
- fn must be a function (not nil)
- fn can have multiple arguments of basic types (int, float, pointer, etc.)
- fn can return at most one value of basic type
- Complex types (string, slice, map, chan, interface) are not supported
- Maximum 2000 callbacks can be registered (program lifetime limit)
Memory Management:
- Callbacks are never freed (stored in global registry)
- This prevents GC from collecting callback data while C code uses it
- For applications with dynamic callback creation, consider callback pools
Usage Example:
func myCallback(x int, y float64) int {
return x + int(y)
}
callbackPtr := ffi.NewCallback(myCallback)
// Pass callbackPtr to C code as function pointer
Using unsafe.Pointer is necessary here as we're creating a function pointer that C code can call. The pointer is obtained from the assembly trampoline table and is guaranteed to be valid for the program lifetime.
func PrepareCallInterface ¶
func PrepareCallInterface( cif *types.CallInterface, convention types.CallingConvention, returnType *types.TypeDescriptor, argTypes []*types.TypeDescriptor, ) error
PrepareCallInterface prepares a function call interface for calling a C function.
This function initializes the CallInterface structure with the necessary metadata for making FFI calls. It must be called before CallFunction.
Parameters:
- cif: Pointer to CallInterface structure to initialize (must not be nil)
- convention: Calling convention (types.DefaultCall, types.CDecl, types.StdCall, etc.)
- returnType: Type descriptor for return value (use types.VoidTypeDescriptor for void)
- argTypes: Slice of type descriptors for each argument (nil or empty slice for no arguments)
Returns:
- nil on success
- ErrInvalidCallInterface if parameters are invalid
- Other errors if type validation or platform preparation fails
Example:
var cif types.CallInterface
err := ffi.PrepareCallInterface(
&cif,
types.DefaultCall,
types.Int32TypeDescriptor,
[]*types.TypeDescriptor{
types.PointerTypeDescriptor,
types.Int32TypeDescriptor,
},
)
For functions with no arguments, pass nil or an empty slice for argTypes:
err := ffi.PrepareCallInterface(&cif, types.DefaultCall, types.VoidTypeDescriptor, nil)
func PrepareVariadicCallInterface ¶ added in v0.5.2
func PrepareVariadicCallInterface( cif *types.CallInterface, convention types.CallingConvention, nfixedargs int, returnType *types.TypeDescriptor, argTypes []*types.TypeDescriptor, ) error
PrepareVariadicCallInterface prepares a call interface for a C variadic function.
nfixedargs is the count of fixed parameters before '...' in the C prototype. argTypes must contain ALL arguments (fixed + variadic) for this specific call. A new CIF must be prepared for each unique combination of variadic argument types.
On Apple ARM64, variadic arguments are forced to the stack per Apple's AAPCS64 extension. The register allocators are exhausted at the fixed/variadic boundary so that variadic arguments land on the stack even when registers are available. On all other platforms, this function behaves identically to PrepareCallInterface.
Example:
// Prepare for: int64_t sum_variadic(int64_t count, ...)
// Called with count=3 and three int64_t variadic args.
var cif types.CallInterface
err := ffi.PrepareVariadicCallInterface(
&cif,
types.DefaultCall,
1, // nfixedargs: only 'count' is fixed
types.SInt64TypeDescriptor,
[]*types.TypeDescriptor{
types.SInt64TypeDescriptor, // count
types.SInt64TypeDescriptor, // arg1 (variadic)
types.SInt64TypeDescriptor, // arg2 (variadic)
types.SInt64TypeDescriptor, // arg3 (variadic)
},
)
Types ¶
type CallingConventionError ¶
type CallingConventionError struct {
Convention int // The invalid convention value
Platform string // Current platform (OS/Arch)
Reason string // Why it's not supported
}
CallingConventionError indicates an unsupported or invalid calling convention.
This error is returned when attempting to use a calling convention that is not supported on the current platform or is invalid.
func (*CallingConventionError) Error ¶
func (e *CallingConventionError) Error() string
func (*CallingConventionError) Is ¶
func (e *CallingConventionError) Is(target error) bool
Is implements error equality for errors.Is().
type InvalidCallInterfaceError ¶
type InvalidCallInterfaceError struct {
Field string // Which field was invalid ("cif", "returnType", "argTypes", etc.)
Reason string // Why it was invalid (human-readable description)
Index int // For array fields like argTypes (-1 if not applicable)
}
InvalidCallInterfaceError indicates CallInterface preparation failed due to invalid parameters.
This error provides detailed information about which field failed validation and why, enabling programmatic error handling and better debugging.
Example:
var icErr *InvalidCallInterfaceError
if errors.As(err, &icErr) {
fmt.Printf("Field %s failed: %s\n", icErr.Field, icErr.Reason)
if icErr.Index >= 0 {
fmt.Printf("At index: %d\n", icErr.Index)
}
}
func (*InvalidCallInterfaceError) Error ¶
func (e *InvalidCallInterfaceError) Error() string
func (*InvalidCallInterfaceError) Is ¶
func (e *InvalidCallInterfaceError) Is(target error) bool
Is implements error equality for errors.Is().
type LibraryError ¶
type LibraryError struct {
Operation string // "load", "symbol", or "free"
Name string // Library path or symbol name
Err error // Underlying OS error (can be nil)
}
LibraryError wraps dynamic library loading and symbol resolution errors.
This error provides context about which library operation failed and includes the underlying OS-specific error for detailed diagnostics.
Example:
var libErr *LibraryError
if errors.As(err, &libErr) {
fmt.Printf("Failed to %s library %q\n", libErr.Operation, libErr.Name)
fmt.Printf("OS error: %v\n", libErr.Err)
}
func (*LibraryError) Error ¶
func (e *LibraryError) Error() string
func (*LibraryError) Is ¶
func (e *LibraryError) Is(target error) bool
Is implements error equality for errors.Is().
func (*LibraryError) Unwrap ¶
func (e *LibraryError) Unwrap() error
Unwrap returns the underlying error for errors.Unwrap().
type TypeValidationError ¶
type TypeValidationError struct {
TypeName string // Name or description of the type
Kind int // The TypeKind value that failed
Reason string // Why validation failed
Index int // For composite types (-1 if not applicable)
}
TypeValidationError indicates a type descriptor failed validation.
This error provides details about which type failed and why, helping users fix type definition issues.
func (*TypeValidationError) Error ¶
func (e *TypeValidationError) Error() string
func (*TypeValidationError) Is ¶
func (e *TypeValidationError) Is(target error) bool
Is implements error equality for errors.Is().
type UnsupportedPlatformError ¶
type UnsupportedPlatformError struct {
OS string // Operating system (e.g., "linux", "windows", "darwin")
Arch string // Architecture (e.g., "amd64", "arm64")
}
UnsupportedPlatformError indicates the current platform is not supported by FFI.
This error is returned when attempting to use FFI on a platform that doesn't have an implementation (e.g., ARM64 before it's fully implemented).
Example:
var upErr *UnsupportedPlatformError
if errors.As(err, &upErr) {
fmt.Printf("Platform %s/%s not supported\n", upErr.OS, upErr.Arch)
}
func (*UnsupportedPlatformError) Error ¶
func (e *UnsupportedPlatformError) Error() string
func (*UnsupportedPlatformError) Is ¶
func (e *UnsupportedPlatformError) Is(target error) bool
Is implements error equality for errors.Is().