Documentation
¶
Overview ¶
Package ffi provides callback support for Foreign Function Interface (Unix version). This file implements Go function registration as C callbacks using pre-compiled assembly trampolines for optimal performance.
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
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 AMD64 (System V ABI)
- Windows AMD64 (Win64 ABI)
- macOS AMD64 (planned)
- ARM64 (planned)
Performance ¶
This implementation uses hand-optimized assembly for each platform's calling convention. Overhead is approximately 50-60ns per call, which is negligible for most use cases (e.g., WebGPU rendering).
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)
- 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
- DO NOT use the same CallInterface from multiple goroutines simultaneously without external synchronization
- 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
Race detector is not supported for zero-CGO libraries (race detector requires CGO_ENABLED=1, which conflicts with our fakecgo implementation using build tag !cgo). This is a fundamental limitation of the zero-CGO approach. However, this library contains no data races in its internal implementation - all shared state (Registry, TypeDescriptors) is initialized once at startup and accessed read-only thereafter.
Zero Dependencies ¶
This package has zero external dependencies (except for internal/fakecgo on Linux). 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 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 (length must match argCount from PrepareCallInterface)
Returns:
- errno: C errno captured after the call (0 on success or Windows)
- err: nil on success; ErrInvalidCallInterface if cif or fn is nil
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 (length must match argCount from PrepareCallInterface)
Returns:
- errno: C errno captured after the call (0 on success or Windows)
- err: nil on success; ctx.Err() if context cancelled; ErrInvalidCallInterface if cif or fn is nil
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.
Safety:
- All argument pointers must remain valid during the call
- Return value buffer must be large enough for the result type
- Use runtime.KeepAlive() if needed to prevent premature GC of arguments
- Use runtime.Pinner to pin pointers under a moving GC
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().