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, ...) error
- func CallFunctionContext(ctx context.Context, cif *types.CallInterface, fn unsafe.Pointer, ...) 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
- 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.
Functions ¶
func CallFunction ¶
func CallFunction( cif *types.CallInterface, fn unsafe.Pointer, rvalue unsafe.Pointer, avalue []unsafe.Pointer, ) 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.
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:
- nil on success
- ErrInvalidCallInterface if cif or fn is nil
- ErrFunctionCallFailed if the call execution fails
Example:
// Calling strlen(const char *str)
var result uintptr
str := "Hello"
err := ffi.CallFunction(
&cif,
strlenPtr,
unsafe.Pointer(&result),
[]unsafe.Pointer{unsafe.Pointer(&str)},
)
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, ) 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.
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:
- nil on success
- ctx.Err() if context is cancelled or deadline exceeded before call starts
- ErrInvalidCallInterface if cif or fn is nil
- ErrFunctionCallFailed if the call execution fails
Example:
// Call with timeout
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
var result float64
arg := 16.0
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
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)
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().