ffi

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2026 License: MIT Imports: 10 Imported by: 6

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

This section is empty.

Variables

View Source
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.

View Source
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,
) 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 NewCallback added in v0.2.0

func NewCallback(fn any) uintptr

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().

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL