This guide provides practical, real-world examples to help you solve common FFI problems and leverage the full power of the infix
library. Where the README.md
covers concepts, this cookbook provides the code.
Note: For a complete reference on the string format used in these examples (e.g., "int"
, "{double, double}"
, "*char"
), please see the Signature Language Reference.
Table of Contents
- Chapter 1: The Basics (Forward Calls)
- Recipe: Calling a Simple C Function
- Recipe: Passing and Receiving Pointers
- Recipe: Working with "Out" Parameters
- Recipe: Working with Opaque Pointers (Incomplete Types)
- Chapter 2: Handling Complex Data Structures
- Recipe: Small Structs Passed by Value
- Recipe: Receiving a Struct from a Function
- Recipe: Large Structs Passed by Reference
- Recipe: Working with Packed Structs
- Recipe: Working with Structs that Contain Bitfields
- Recipe: Working with Unions
- Recipe: Working with Fixed-Size Arrays
- Recipe: Advanced Named Types (Recursive & Forward-Declared)
- Recipe: Working with Complex Numbers
- Recipe: Working with SIMD Vectors
- Recipe: Working with Enums
- Chapter 3: The Power of Callbacks (Reverse Calls)
- Recipe: Creating a Type-Safe Callback for `qsort`
- Recipe: Creating a Stateful Callback
- Chapter 4: Advanced Techniques
- Recipe: Calling Variadic Functions like `printf`
- Recipe: Receiving and Calling a Function Pointer
- Recipe: Calling a Function Pointer from a Struct (V-Table Emulation)
- Recipe: Handling `longdouble`
- Recipe: Proving Reentrancy with Nested FFI Calls
- Recipe: Proving Thread Safety
- Chapter 5: Interoperability with Other Languages
- The Universal Principle: The C ABI
- Recipe: Interfacing with a C++ Class (Directly)
- Recipe: Interfacing with C++ Templates
- The Pattern for Other Compiled Languages
- Rust
- Zig
- Go
- Swift
- Dlang
- Fortran
- Assembly
- Chapter 6: Dynamic Libraries & System Calls
- Recipe: Calling Native System Libraries without Linking
- Recipe: Reading and Writing Global Variables
- Example 1: Simple Integer Variable
- Example 2: Aggregate (Struct) Variable
- Recipe: Handling Library Dependencies
- Chapter 7: Introspection for Data Marshalling
- Recipe: Dynamic Struct Marshalling with the Signature Parser
- Recipe: Building a Signature String at Runtime
- Recipe: Introspecting a Trampoline for a Wrapper
- Chapter 8: Performance & Memory Management
- Best Practice: Caching Trampolines
- Recipe: Using a Custom Arena for a Group of Types
- Recipe: The Full Manual API Lifecycle (Types to Trampoline)
- Recipe: Using Custom Memory Allocators
- Recipe: Building a Dynamic Call Frame with an Arena
- How It Works & Why It's Better
- Advanced Optimization: Arena Resetting for Hot Loops
- Chapter 9: Common Pitfalls & Troubleshooting
- Recipe: Advanced Error Reporting for the Parser
- Mistake: Passing a Value Instead of a Pointer in `args[]`
- Mistake: `infix` Signature Mismatch
- Pitfall: Function Pointer Syntax
- Chapter 10: A Comparative Look: `infix` vs. `libffi` and `dyncall`
- Scenario 1: Calling a Simple Function
- The `dyncall` Approach
- The `libffi` Approach
- The `infix` Approach
- Scenario 2: Calling a Function with a Struct
- The `dyncall` Approach
- The `libffi` Approach
- The `infix` Approach
- Scenario 3: Creating a Callback
- The `dyncall` Approach
- The `libffi` Approach
- The `infix` Approach
- Analysis and Takeaways
- Chapter 11: Building Language Bindings
- The Four Pillars of a Language Binding
- Recipe: Porting a Python Binding from `dyncall` to `infix`
Chapter 1: The Basics (Forward Calls)
Recipe: Calling a Simple C Function
Problem: You want to call a standard C function, like atan2
from the math library.
Solution: Describe the function's signature, prepare pointers to your arguments, and invoke the function through a generated trampoline. An "unbound" trampoline is ideal when you want to call multiple functions that share the same signature.
#include <math.h>
#include <stdio.h>
void recipe_simple_forward_call() {
const char* signature = "(double, double) -> double";
double y = 1.0, x = 1.0;
void*
args[] = { &y, &x };
double result;
cif((
void*)atan2, &result,
args);
printf("atan2(1.0, 1.0) = %f (PI/4)\n", result);
}
void * args[]
Definition 202_in_structs.c:75
c23_nodiscard infix_status infix_forward_create_unbound(infix_forward_t **, const char *, infix_registry_t *)
Generates an unbound forward-call trampoline from a signature string.
Definition trampoline.c:687
c23_nodiscard infix_unbound_cif_func infix_forward_get_unbound_code(infix_forward_t *)
Retrieves the executable code pointer from an unbound forward trampoline.
Definition trampoline.c:217
void infix_forward_destroy(infix_forward_t *)
Frees a forward trampoline and its associated executable memory.
Definition trampoline.c:406
void(* infix_unbound_cif_func)(void *, void *, void **)
The signature for a generic "unbound" forward-call trampoline.
Definition infix.h:315
Definition infix_internals.h:72
Recipe: Passing and Receiving Pointers
Problem: You need to call a C function that takes a pointer as an argument and returns a pointer, like strchr
.
Solution: Use the *
prefix for pointer types. The value in the args
array for a pointer argument is the address of your pointer variable.
#include <string.h>
#include <stdio.h>
void recipe_pointer_args_and_return() {
const char* signature = "(*char, int) -> *char";
const char* haystack = "hello-world";
int needle = '-';
void*
args[] = { &haystack, &needle };
const char* result_ptr = NULL;
if (result_ptr) {
printf("strchr found: '%s'\n", result_ptr);
}
}
c23_nodiscard infix_status infix_forward_create(infix_forward_t **, const char *, void *, infix_registry_t *)
Generates a bound forward-call trampoline from a signature string.
Definition trampoline.c:656
c23_nodiscard infix_cif_func infix_forward_get_code(infix_forward_t *)
Retrieves the executable code pointer from a bound forward trampoline.
Definition trampoline.c:227
void(* infix_cif_func)(void *, void **)
The signature for a "bound" forward-call trampoline.
Definition infix.h:335
Recipe: Working with "Out" Parameters
Problem: You need to call a C function that doesn't use its return value for its primary output. Instead, it takes a pointer to a variable and writes the result into it. This is a very common pattern for functions that need to return multiple values or an error code.
Solution: The signature is straightforward. The "out" parameter is simply a pointer type (*<type>
). In your calling code, you create a local variable and pass its address in the args
array.
bool get_user_stats(int user_id, int* out_posts, double* out_score) {
if (user_id == 123) {
if(out_posts) *out_posts = 50;
if(out_score) *out_score = 99.8;
return true;
}
return false;
}
void recipe_out_parameters() {
const char* signature = "(int, *int, *double) -> bool";
int user_id = 123;
int post_count = 0;
double score = 0.0;
bool success;
void*
args[] = { &user_id, &post_count, &score };
if (success) {
printf("User stats: Posts=%d, Score=%.1f\n", post_count, score);
}
}
Recipe: Working with Opaque Pointers (Incomplete Types)
Problem: You need to interact with a C library that uses opaque pointers or handles (e.g., FILE*
, sqlite3*
) where the internal structure is hidden.
Solution: Use the *void
signature. This is the canonical representation for any generic handle. Using a registry to create a type alias like @FileHandle = *void;
can make your signatures more readable.
#include <stdio.h>
void recipe_opaque_pointers() {
void* file_handle = NULL;
const char* filename = "test.txt";
const char* mode = "w";
void* fopen_args[] = { &filename, &mode };
if (file_handle) {
const char* content = "Written by infix!";
void* fputs_args[] = { &content, &file_handle };
int result;
printf("Successfully wrote to test.txt\n");
}
}
void infix_registry_destroy(infix_registry_t *registry)
Frees a type registry and all type definitions and metadata contained within it.
Definition type_registry.c:166
c23_nodiscard infix_status infix_register_types(infix_registry_t *, const char *)
Parses a string of definitions and populates a type registry.
Definition type_registry.c:233
c23_nodiscard infix_registry_t * infix_registry_create(void)
Creates a new, empty type registry.
Definition type_registry.c:131
Definition infix_internals.h:158
Chapter 2: Handling Complex Data Structures
typedef struct {
double x, y; }
Point;
A simple struct with two doubles (16 bytes).
Definition types.h:39
Recipe: Small Structs Passed by Value
Problem: You need to call a function that takes a small struct
that the ABI passes in registers. Solution: Use the struct syntax ({...})
. infix
will automatically determine the correct ABI passing convention for the target platform.
void recipe_pass_struct_by_value() {
const char* signature = "({double, double}, double) -> {double, double}";
double delta_x = 5.5;
printf(
"Moved point has x = %f\n",
end.x);
}
Point move_point(Point p, double dx, double dy)
A simple function that takes and returns a small struct by value.
Definition 006_end_to_end_calls.c:19
clock_t start
Definition 901_call_overhead.c:58
clock_t end
Definition 901_call_overhead.c:58
double x
Definition types.h:40
Recipe: Receiving a Struct from a Function
Problem: You need to call a function that returns a struct by value.
Solution: infix
handles the ABI details, whether the struct is returned in registers or via a hidden pointer passed by the caller.
Point make_point(
double x,
double y) {
return (
Point){x, y}; }
void recipe_return_struct() {
const char* signature = "(double, double) -> {double, double}";
double x = 100.0, y = 200.0;
void*
args[] = { &x, &y };
printf(
"Received point: {x=%.1f, y=%.1f}\n", result.
x, result.
y);
}
double y
Definition types.h:41
Recipe: Large Structs Passed by Reference
Problem: A function takes a struct that is too large to fit in registers.
Solution: The process is identical to the small struct example. infix
's ABI logic will detect that the struct is large and automatically pass it by reference (the standard C ABI rule).
int get_first_element(
LargeStruct s) {
return s.data[0]; }
void recipe_large_struct() {
const char* signature = "({[8:int]}) -> int";
LargeStruct my_struct = { {123, -1, -1, -1, -1, -1, -1, -1} };
void*
args[] = { &my_struct };
int result;
printf("First element of large struct: %d\n", result);
}
A struct with a size greater than 16 bytes.
Definition types.h:84
Recipe: Working with Packed Structs
Problem: You need to call a function that takes a __attribute__((packed))
struct.
Solution: Use the !{...}
syntax for 1-byte alignment, or !N:{...}
to specify a maximum alignment of N
bytes.
#pragma pack(push, 1)
typedef struct { char a; uint64_t b; } Packed;
#pragma pack(pop)
int process_packed(Packed p) { return (p.a == 'X' && p.b == 0x1122334455667788ULL) ? 42 : -1; }
void recipe_packed_struct() {
const char* signature = "(!{char, uint64}) -> int";
Packed p = {'X', 0x1122334455667788ULL};
int result = 0;
printf("Packed struct result: %d\n", result);
}
Recipe: Working with Structs that Contain Bitfields
Problem: You need to interact with a C struct that uses bitfields. infix
's signature language has no syntax for bitfields because their memory layout is implementation-defined and not portable.
Solution: Treat the underlying integer that holds the bitfields as a single member in your signature. Then, use C's bitwise operators in your wrapper code to manually pack and unpack the values before and after the FFI call.
typedef struct {
uint32_t a : 4;
uint32_t b : 12;
uint32_t c : 16;
} BitfieldStruct;
uint32_t process_bitfields(BitfieldStruct s) {
return s.a + s.b + s.c;
}
void recipe_bitfields() {
const char* signature = "({uint32}) -> uint32";
uint32_t packed_data = 0;
packed_data |= (15 & 0xF) << 0;
packed_data |= (1000 & 0xFFF) << 4;
packed_data |= (30000 & 0xFFFF) << 16;
void*
args[] = { &packed_data };
uint32_t result;
printf("Bitfield sum: %u\n", result);
}
Recipe: Working with Unions
Problem: You need to call a function that passes or returns a union
.
Solution: Use the <...>
syntax to describe the union's members.
typedef union {
int i;
float f; }
Number;
int process_number_as_int(
Number n) {
return n.
i * 2; }
void recipe_union() {
const char* signature = "(<int, float>) -> int";
int result = 0;
void*
args[] = {&num_val};
printf("Result: %d\n", result);
}
A simple union of an integer and a float.
Definition types.h:72
int i
Definition types.h:73
Recipe: Working with Fixed-Size Arrays
Problem: A function takes a fixed-size array, like long long sum(long long arr[4]);
.
Solution: In C, an array argument "decays" to a pointer to its first element. The signature must reflect this. To describe the array itself (e.g., inside a struct), use the [N:type]
syntax.
int64_t sum_array_elements(const int64_t* arr, size_t count) {
int64_t sum = 0;
for(size_t i = 0; i < count; ++i) sum += arr[i];
return sum;
}
void recipe_array_decay() {
const char* signature = "(*sint64, sint64) -> sint64";
int64_t my_array[] = {10, 20, 30, 40};
const int64_t* ptr_to_array = my_array;
int64_t count = 4;
void*
args[] = {&ptr_to_array, &count};
int64_t result = 0;
printf("Sum of array is: %lld\n", (long long)result);
}
Recipe: Advanced Named Types (Recursive & Forward-Declared)
Problem: You need to describe complex, real-world C data structures, such as a linked list or mutually dependent types.
Solution: The infix
registry fully supports recursive definitions and forward declarations, allowing you to model these patterns cleanly.
#include <stdio.h>
typedef struct Employee Employee;
typedef struct Manager {
const char* name;
Employee* reports[10];
} Manager;
struct Employee {
const char* name;
Manager* manager;
};
const char* get_manager_name(Employee* e) {
return e->manager ? e->manager->name : "None";
}
void recipe_advanced_registry() {
const char* definitions =
"@Employee; @Manager;"
"@Manager = { name:*char, reports:[10:*@Employee] };"
"@Employee = { name:*char, manager:*@Manager };"
;
Manager boss = { "Sanko", { NULL } };
Employee worker = { "Robinson", &boss };
Employee* p_worker = &worker;
const char* manager_name = NULL;
void*
args[] = { &p_worker };
printf("The manager of %s is %s.\n", worker.name, manager_name);
}
Recipe: Working with Complex Numbers
Problem: You need to call a C function that uses _Complex
types.
Solution: Use the c[<base_type>]
constructor in the signature string.
#include <complex.h>
double complex c_square(double complex z) { return z * z; }
void recipe_complex() {
const char* signature = "(c[double]) -> c[double]";
double complex input = 3.0 + 4.0 * I;
double complex result;
printf("The square of (3.0 + 4.0i) is (%.1f + %.1fi)\n", creal(result), cimag(result));
}
Recipe: Working with SIMD Vectors
Problem: You need to call a high-performance C function that uses SIMD vector types.
Solution: Use the v[<elements>:<type>]
syntax. The ABI logic will ensure the vector is passed in a SIMD register.
#include <emmintrin.h>
__m128d vector_add(__m128d a, __m128d b) { return _mm_add_pd(a, b); }
void recipe_simd() {
const char* signature = "(v[2:double], v[2:double]) -> v[2:double]";
__m128d a = _mm_set_pd(20.0, 10.0);
__m128d b = _mm_set_pd(22.0, 32.0);
__m128d result;
double* d = (double*)&result;
printf("SIMD vector result: [%.1f, %.1f]\n", d[0], d[1]);
}
Recipe: Working with Enums
Problem: You need to call a C function that takes an enum
, but you want to ensure the underlying integer type is handled correctly for ABI purposes.
Solution: Use the e:<type>
syntax in the signature string. infix
treats the enum identically to its underlying integer type for the FFI call, which is the correct behavior.
typedef enum { STATUS_OK = 0, STATUS_WARN = 1, STATUS_ERR = -1 } StatusCode;
const char* status_to_string(StatusCode code) {
switch (code) {
case STATUS_OK: return "OK";
case STATUS_WARN: return "Warning";
case STATUS_ERR: return "Error";
default: return "Unknown";
}
}
void recipe_enums() {
const char* signature = "(e:int) -> *char";
int code = STATUS_ERR;
const char* result_str = NULL;
void*
args[] = { &code };
printf("Status for code %d is '%s'\n", code, result_str);
}
Chapter 3: The Power of Callbacks (Reverse Calls)
Recipe: Creating a Type-Safe Callback for <tt>qsort</tt>
Problem: You need to sort an array using C's qsort
, which requires a function pointer for the comparison logic.
Solution: Use infix_reverse_create_callback
. The handler is a normal, clean C function whose signature exactly matches what qsort
expects.
#include <stdlib.h>
int compare_integers_handler(const void* a, const void* b) {
return (*(const int*)a - *(const int*)b);
}
void recipe_qsort_callback() {
const char* cmp_sig = "(*void, *void) -> int";
typedef int (*compare_func_t)(const void*, const void*);
int numbers[] = { 5, 1, 4, 2, 3 };
size_t num_count = sizeof(numbers) / sizeof(numbers[0]);
qsort(numbers, num_count, sizeof(int), my_comparator);
printf("Sorted numbers:");
for(size_t i = 0; i < num_count; ++i) {
printf(" %d", numbers[i]);
}
printf("\n");
}
c23_nodiscard infix_status infix_reverse_create_callback(infix_reverse_t **, const char *, void *, infix_registry_t *)
Generates a type-safe reverse-call trampoline (callback) from a signature string.
Definition trampoline.c:696
c23_nodiscard void * infix_reverse_get_code(const infix_reverse_t *)
Retrieves the executable code pointer from a reverse trampoline.
Definition trampoline.c:636
void infix_reverse_destroy(infix_reverse_t *)
Frees a reverse trampoline, its JIT-compiled stub, and its context.
Definition trampoline.c:622
Definition infix_internals.h:102
Recipe: Creating a Stateful Callback
Problem: A callback handler needs access to application state, but the C library API is stateless (it has no void* user_data
parameter).
Solution: Use infix_reverse_create_closure
. This API is specifically designed for stateful callbacks. You provide a generic handler and a void* user_data
pointer to your state. Inside the handler, you can retrieve this pointer from the context
.
typedef struct { const char * name; int sum; } AppContext;
(void)ret;
int item_value = *(
int*)
args[0];
ctx->sum += item_value;
}
typedef void (*item_processor_t)(int);
void process_list(const int* items, int count, item_processor_t process_func) {
for (int i = 0; i < count; ++i) process_func(items[i]);
}
void recipe_stateful_callback() {
AppContext ctx = {"My List", 0};
int list[] = {10, 20, 30};
process_list(list, 3, processor_ptr);
printf("Final sum for '%s': %d\n", ctx.name, ctx.sum);
}
c23_nodiscard infix_status infix_reverse_create_closure(infix_reverse_t **, const char *, infix_closure_handler_fn, void *, infix_registry_t *)
Generates a low-level reverse-call trampoline (closure) from a signature string.
Definition trampoline.c:726
c23_nodiscard void * infix_reverse_get_user_data(const infix_reverse_t *)
Retrieves the user_data stored with a reverse trampoline.
Definition trampoline.c:645
Chapter 4: Advanced Techniques
Recipe: Calling Variadic Functions like <tt>printf</tt>
Problem: You need to call a function with a variable number of arguments.
Solution: Use the ;
token to separate fixed and variadic arguments. The signature must exactly match the types you are passing in a specific call.
void recipe_variadic_printf() {
const char* signature = "(*char; int, double) -> int";
const char* fmt = "Count: %d, Value: %.2f\n";
int count = 42;
double value = 123.45;
void*
args[] = { &fmt, &count, &value };
int result;
}
Recipe: Receiving and Calling a Function Pointer
Problem: You need to call a C function that takes a function pointer as an argument, and pass it a callback you generate.
Solution: The signature for a function pointer is *((...) -> ...)
. Generate your callback, get its native pointer, and pass that pointer as an argument.
int multiply_handler(int x) { return x * 10; }
int harness_func(int (*worker_func)(int), int base_val) { return worker_func(base_val); }
void recipe_callback_as_arg() {
infix_forward_create(&harness_trampoline,
"(*((int)->int), int) -> int", (
void*)harness_func, NULL);
int value = 7;
void* harness_args[] = { &inner_cb_ptr, &value };
int result;
printf("Result from nested callback: %d\n", result);
}
Recipe: Calling a Function Pointer from a Struct (V-Table Emulation)
Problem: You have a pointer to a struct that contains function pointers, similar to a C implementation of an object's v-table. You need to read a function pointer from the struct and then call it.
Solution: This is a two-step FFI process. First, read the function pointer value from the struct. Second, create a new trampoline for that function pointer's signature and call it. The Type Registry is perfect for making this clean.
typedef struct { int val; } Adder;
int vtable_add(Adder* self, int amount) { return self->val + amount; }
void vtable_destroy(Adder* self) { free(self); }
typedef struct {
int (*add)(Adder* self, int amount);
void (*destroy)(Adder* self);
} AdderVTable;
const AdderVTable VTABLE = { vtable_add, vtable_destroy };
Adder* create_adder(int base) {
Adder* a = malloc(sizeof(Adder));
if (a) a->val = base;
return a;
}
void recipe_vtable_call() {
"@Adder = { val: int };"
"@Adder_add_fn = (*@Adder, int) -> int;"
"@AdderVTable = { add: *@Adder_add_fn };"
);
Adder* my_adder = create_adder(100);
const AdderVTable* vtable = &VTABLE;
void* add_func_ptr = (void*)vtable->add;
int amount_to_add = 23;
int result;
void* add_args[] = { &my_adder, &amount_to_add };
printf("Result from v-table call: %d\n", result);
free(my_adder);
}
Recipe: Handling <tt>longdouble</tt>
Problem: You need to call a function that uses longdouble
, which has different sizes and ABI rules on different platforms (e.g., 80-bit on x86, 128-bit on AArch64, or just an alias for double
on MSVC/macOS).
Solution: Use the longdouble
keyword in your signature. infix
's ABI logic contains the platform-specific rules to handle it correctly, whether it's passed on the x87 FPU stack (System V x64), in a 128-bit vector register (AArch64), or as a normal double
.
#include <math.h>
long double native_sqrtl(long double x) {
return sqrtl(x);
}
void recipe_long_double() {
const char* signature = "(longdouble) -> longdouble";
long double input = 144.0L;
long double result = 0.0L;
void*
args[] = { &input };
printf("sqrtl(144.0L) = %Lf\n", result);
}
Recipe: Proving Reentrancy with Nested FFI Calls
Problem: You need to be sure that making an FFI call from within an FFI callback is safe.
Solution: infix
is designed to be fully reentrant. The library uses no global mutable state, and all error information is stored in thread-local storage. This recipe demonstrates a forward call that invokes a reverse callback, which in turn makes another forward call.
#include <string.h>
int multiply(int a, int b) { return a * b; }
int val = *(
int*)
args[0];
int multiplier = 5;
void* mult_args[] = { &val, &multiplier };
int result;
memcpy(ret, &result, sizeof(int));
}
int harness(int (*func)(int), int input) {
return func(input);
}
void recipe_reentrancy() {
const char* harness_sig = "(*((int)->int), int)->int";
int base_val = 8;
void* harness_args[] = { &callback_ptr, &base_val };
int final_result;
printf("Nested/reentrant call result: %d\n", final_result);
}
Recipe: Proving Thread Safety
Problem: You need to create a trampoline in one thread and safely use it in another.
Solution: infix
trampoline handles (infix_forward_t*
and infix_reverse_t*
) are immutable after creation and are safe to share between threads. All error state is kept in thread-local storage, so calls from different threads will not interfere with each other.
#include <stdio.h>
#if defined(_WIN32)
#include <windows.h>
#else
#include <pthread.h>
#endif
int add(int a, int b) { return a + b; }
typedef struct {
int result;
} thread_data_t;
#if defined(_WIN32)
DWORD WINAPI worker_thread_func(LPVOID arg) {
#else
void* worker_thread_func(void* arg) {
#endif
thread_data_t* data = (thread_data_t*)arg;
int a = 20, b = 22;
void*
args[] = { &a, &b };
data->cif(&data->result,
args);
#if defined(_WIN32)
return 0;
#else
return NULL;
#endif
}
void recipe_thread_safety() {
#if defined(_WIN32)
HANDLE thread_handle = CreateThread(NULL, 0, worker_thread_func, &data, 0, NULL);
WaitForSingleObject(thread_handle, INFINITE);
CloseHandle(thread_handle);
#else
pthread_t thread_id;
pthread_create(&thread_id, NULL, worker_thread_func, &data);
pthread_join(thread_id, NULL);
#endif
printf("Result from worker thread: %d\n", data.result);
}
Chapter 5: Interoperability with Other Languages
The Universal Principle: The C ABI
infix
can call any function that exposes a standard C ABI. Nearly every compiled language provides a mechanism to export a function using this standard (extern "C"
in C++/Rust/Zig, //export
in Go, bind(C)
in Fortran).
Recipe: Interfacing with a C++ Class (Directly)
Problem: You need to call C++ class methods without writing a C wrapper.
Solution: Find the compiler-mangled names for the constructor, destructor, and methods. Use infix
to call them directly, manually passing the this
pointer as the first argument to methods.
#include <iostream>
class MyClass {
int value;
public:
MyClass(int val) : value(val) { std::cout << "C++ Constructor called.\n"; }
~MyClass() { std::cout << "C++ Destructor called.\n"; }
int add(int x) { return this->value + x; }
};
extern "C" size_t get_myclass_size() { return sizeof(MyClass); }
#include <stdio.h>
#include <stdlib.h>
#if defined(__GNUC__) || defined(__clang__)
const char* MANGLED_CONSTRUCTOR = "_ZN7MyClassC1Ei";
const char* MANGLED_DESTRUCTOR = "_ZN7MyClassD1Ev";
const char* MANGLED_ADD = "_ZN7MyClass3addEi";
#elif defined(_MSC_VER)
const char* MANGLED_CONSTRUCTOR = "??0MyClass@@QEAA@H@Z";
const char* MANGLED_DESTRUCTOR = "??1MyClass@@QEAA@XZ";
const char* MANGLED_ADD = "?add@MyClass@@QEAAHH@Z";
#endif
void recipe_cpp_mangled() {
if (!lib) return;
if (!p_ctor || !p_dtor || !p_add || !p_size) {
printf("Failed to find one or more symbols.\n");
return;
}
void* obj = malloc(p_size());
int initial_val = 100;
int add_val = 23, result;
printf("C++ mangled method returned: %d\n", result);
free(obj);
}
void infix_library_close(infix_library_t *)
Closes a dynamic library handle and unloads it.
Definition loader.c:113
c23_nodiscard void * infix_library_get_symbol(infix_library_t *, const char *)
Retrieves the memory address of a symbol (function or global variable).
Definition loader.c:144
c23_nodiscard infix_library_t * infix_library_open(const char *)
Opens a dynamic library and returns a handle to it.
Definition loader.c:65
Recipe: Interfacing with C++ Templates
Problem: How do you call a C++ function template from C?
Solution: You can't call the template itself, but you can call a specific instantiation of it. The compiler generates a normal function for each concrete type used with the template, and this function has a predictable mangled name that you can look up and call.
#include <iostream>
template <typename T>
class Box {
T value;
public:
Box(T val) : value(val) {}
T get_value() { return this->value; }
};
template class Box<int>;
template class Box<double>;
#include <stdio.h>
#include <stdlib.h>
const char* MANGLED_GET_DBL = "_ZNK3BoxIdE9get_valueEv";
void recipe_cpp_template() {
if (!lib) return;
double val = 3.14;
void* my_box = malloc(sizeof(double));
memcpy(my_box, &val, sizeof(double));
if (!p_get_value) {
printf("Failed to find mangled template function.\n");
free(my_box);
return;
}
double result;
printf("Value from C++ template object: %f\n", result);
free(my_box);
}
The Pattern for Other Compiled Languages
The extern "C"
pattern is universal. The C code to call any of the functions below would be identical: load the library, find the symbol, create a trampoline for (int, int) -> int
, and call it.
Rust
To export a C-compatible function from Rust, use #[no_mangle]
to prevent name mangling and extern "C"
to specify the calling convention.
// librust_math.rs
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
Compile with: rustc --crate-type cdylib librust_math.rs
Zig
Zig's export
keyword makes a function available with the C ABI by default.
// libzig_math.zig
export fn zig_add(a: c_int, b: c_int) c_int {
return a + b;
}
Compile with: zig build-lib -dynamic libzig_math.zig
Go
Go can export functions to C using a special //export
comment directive.
// libgo_math.go
package main
import "C"
//export go_add
func go_add(a C.int, b C.int) C.int { return a + b }
func main() {}
Compile with: go build -buildmode=c-shared -o libgo_math.so libgo_math.go
Swift
The @_cdecl
attribute exposes a Swift function to C with a specified name.
// libswift_math.swift
@_cdecl("swift_add")
public func swift_add(a: CInt, b: CInt) -> CInt {
return a + b
}
Compile with: swiftc -emit-library libswift_math.swift -o libswift_math.so
Dlang
The extern(C)
attribute specifies the C calling convention for a D function.
extern (C) int d_add(int a, int b) { return a + b; }
Compile with: dmd -shared -fPIC -of=libd_math.so libd_math.d
Fortran
The bind(C)
attribute from the iso_c_binding
module provides C interoperability.
function fortran_add(a, b) result(c) bind(C, name='fortran_add')
use iso_c_binding
integer(c_int), value :: a, b
integer(c_int) :: c
c = a + b
end function fortran_add
Compile with: gfortran -shared -fPIC -o libfortran_math.so libfortran_math.f90
Assembly
Pure machine code has no name mangling. You just need to follow the target ABI's calling convention.
; libasm_math.asm (for System V x64 ABI)
section .text
global asm_add
asm_add:
mov eax, edi ; Move first argument (RDI) into EAX
add eax, esi ; Add second argument (RSI) to EAX
ret ; Return value is in EAX
Compile with: nasm -f elf64 libasm_math.asm && gcc -shared -o libasm_math.so libasm_math.o
Chapter 6: Dynamic Libraries & System Calls
Recipe: Calling Native System Libraries without Linking
Problem: You need to call a function from a system library (e.g., user32.dll
) without linking against its import library at compile time.
Solution: Use infix
's cross-platform library loading API to get a handle to the library and the function pointer, then create a trampoline.
#include <stdio.h>
#if defined(_WIN32)
#include <windows.h>
void recipe_system_call() {
if (!user32) {
printf("Failed to open user32.dll\n");
return;
}
if (!pMessageBoxA) {
printf("Failed to find MessageBoxA\n");
return;
}
const char* sig = "(*void, *char, *char, uint32) -> int";
void* hwnd = NULL;
const char* text = "Hello from a dynamically loaded function!";
const char* caption = "infix FFI";
uint32_t type = 0;
void*
args[] = { &hwnd, &text, &caption, &type };
int result;
}
#else
void recipe_system_call() {
printf("This recipe is for Windows only.\n");
}
#endif
Recipe: Reading and Writing Global Variables
Problem: You need to access a global variable exported from a shared library, not just a function.
Solution: Use infix_read_global()
and infix_write_global()
. The powerful signature language is used to describe the variable's type, ensuring infix
reads or writes the correct number of bytes.
Example 1: Simple Integer Variable
First, create a simple shared library (libglobals.c
) that exports a counter:
#if defined(_WIN32)
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
EXPORT int global_counter = 42;
Now, the C code to interact with it:
#include <stdio.h>
void recipe_global_int() {
if (!lib) return;
int counter_val = 0;
printf("Initial value of global_counter: %d\n", counter_val);
int new_val = 100;
counter_val = 0;
printf("New value of global_counter: %d\n", counter_val);
}
infix_status infix_write_global(infix_library_t *, const char *, const char *, void *)
Writes a value to a global variable in a loaded library.
Definition loader.c:213
infix_status infix_read_global(infix_library_t *, const char *, const char *, void *)
Reads the value of a global variable from a loaded library.
Definition loader.c:170
Example 2: Aggregate (Struct) Variable
Let's expand libglobals.c
to export a configuration struct:
typedef struct {
const char* name;
int version;
} Config;
EXPORT Config g_config = { "default", 1 };
Now, the C code to read and write this struct:
#include <stdio.h>
#include <string.h>
typedef struct { const char* name; int version; } Config;
void recipe_global_struct() {
if (!lib) return;
Config local_config;
printf("Initial config: name='%s', version=%d\n", local_config.name, local_config.version);
Config new_config = { "updated", 2 };
memset(&local_config, 0, sizeof(Config));
printf("Updated config: name='%s', version=%d\n", local_config.name, local_config.version);
}
Recipe: Handling Library Dependencies
Problem: You want to load a library (libA
) that itself depends on another shared library (libB
).
Solution: You don't have to do anything special. On all modern operating systems, the dynamic linker will automatically find, load, and link libB
when you load libA
.
int helper_from_lib_b() { return 100; }
int helper_from_lib_b();
int entry_point_a() { return 200 + helper_from_lib_b(); }
void recipe_library_dependencies() {
if (!lib) return;
int result;
printf("Result from chained libraries: %d\n", result);
}
Chapter 7: Introspection for Data Marshalling
Recipe: Dynamic Struct Marshalling with the Signature Parser
Problem: You have data from a dynamic source (like a script) and need to pack it into a C struct
layout at runtime.
Solution: Use infix_type_from_signature
to parse a signature into a detailed infix_type
graph. This graph contains all the size
, alignment
, and member offset
information needed to correctly write data into a C-compatible memory buffer.
#include <stdio.h>
#include <string.h>
#include <stdint.h>
typedef struct { int32_t user_id; double score; const char* name; } UserProfile;
void marshal_ordered_data(void* dest, const char* sig, void** src) {
}
}
void recipe_dynamic_packing() {
int32_t id = 123;
double score = 98.6;
const char* name = "Sanko";
void* my_data[] = { &id, &score, &name };
const char* profile_sig = "{id:int32, score:double, name:*char}";
UserProfile profile_buffer = {0};
marshal_ordered_data(&profile_buffer, profile_sig, my_data);
printf("Resulting C struct: id=%d, score=%.1f, name=%s\n",
profile_buffer.user_id, profile_buffer.score, profile_buffer.name);
}
infix_arena_t * arena
Definition 005_layouts.c:57
c23_nodiscard infix_status infix_type_from_signature(infix_type **, infix_arena_t **, const char *, infix_registry_t *)
Parses a signature string representing a single data type.
Definition signature.c:1090
c23_nodiscard size_t infix_type_get_size(const infix_type *)
Retrieves the size of an infix_type in bytes.
Definition types.c:728
c23_nodiscard size_t infix_type_get_member_count(const infix_type *)
Retrieves the number of members in an aggregate type (struct or union).
Definition types.c:745
c23_nodiscard const infix_struct_member * infix_type_get_member(const infix_type *, size_t)
Retrieves a specific member from an aggregate type by its index.
Definition types.c:756
void infix_arena_destroy(infix_arena_t *)
Frees an entire memory arena and all objects allocated within it.
Definition arena.c:68
@ INFIX_SUCCESS
The operation completed successfully.
Definition infix.h:352
Definition infix_internals.h:130
Describes a single member of an aggregate type (struct or union).
Definition infix.h:219
infix_type * type
An infix_type describing the member's type.
Definition infix.h:221
size_t offset
The byte offset of the member from the start of the aggregate.
Definition infix.h:222
The central structure for describing any data type in the FFI system.
Definition infix.h:161
Recipe: Building a Signature String at Runtime
Problem: The structure of the data you need to work with isn't known until runtime (e.g., it's defined in a configuration file or a user script).
Solution: Since infix
signatures are just strings, you can build them dynamically using snprintf
. You can then parse this dynamic signature to get layout information, which is perfect for data marshalling or dynamic RPC systems.
const char* user_defined_fields[] = { "int", "int", "double" };
int num_fields = 3;
void recipe_dynamic_signature() {
char signature_buffer = "{";
char* current = signature_buffer + 1;
size_t remaining = sizeof(signature_buffer) - 1;
for (int i = 0; i < num_fields; ++i) {
int written = snprintf(current, remaining, "%s%s", user_defined_fields[i], (i == num_fields - 1) ? "" : ",");
if (written < 0 || (size_t)written >= remaining) {
printf("Error: Signature buffer too small.\n");
return;
}
current += written;
remaining -= written;
}
strcat(signature_buffer, "}");
printf("Dynamically generated signature: %s\n", signature_buffer);
if (dynamic_type) {
}
}
Recipe: Introspecting a Trampoline for a Wrapper
Problem: You are building a language binding and need to validate the number and types of arguments provided by the user before making an FFI call.
Solution: Use the trampoline introspection API to query the signature information stored in the handle.
void dynamic_wrapper(
infix_forward_t* trampoline,
void* target_func,
void**
args,
size_t num_provided_args) {
fprintf(stderr, "Error: Incorrect number of arguments. Expected %zu, got %zu.\n",
return;
}
}
c23_nodiscard size_t infix_forward_get_num_args(const infix_forward_t *)
Retrieves the number of arguments for a forward trampoline.
Definition types.c:793
Chapter 8: Performance & Memory Management
Best Practice: Caching Trampolines
Rule: NEVER generate a new trampoline for the same function signature inside a hot loop. The performance of infix
comes from amortizing the one-time generation cost over many fast calls.
int result;
for (int i = 0; i < 1000000; ++i) {
}
Recipe: Using a Custom Arena for a Group of Types
Goal: Create a set of related infix_type
objects for the Manual API and free them all at once.
void recipe_custom_arena() {
}
c23_nodiscard infix_arena_t * infix_arena_create(size_t)
Creates and initializes a new memory arena.
Definition arena.c:41
c23_nodiscard infix_status infix_type_create_array(infix_arena_t *, infix_type **, infix_type *, size_t)
Creates a new infix_type for a fixed-size array from an arena.
Definition types.c:254
c23_nodiscard infix_type * infix_type_create_primitive(infix_primitive_type_id)
Creates an infix_type descriptor for a primitive C type.
Definition types.c:97
@ INFIX_PRIMITIVE_SINT32
signed int, int32_t
Definition infix.h:140
Recipe: The Full Manual API Lifecycle (Types to Trampoline)
Problem: You want to create a trampoline without using the signature parser, for maximum performance or because your type information is already structured in C.
Solution: Use an arena to build your infix_type
objects and then pass them directly to the _manual
variant of the creation functions.
#include <stdio.h>
#include <stddef.h>
typedef struct {
double x, y; }
Point;
void recipe_full_manual_api() {
};
point_type,
};
double delta_x = 5.5;
printf(
"Manual API result: Moved point has x = %f\n",
end.x);
}
infix_type * arg_types[]
Definition 901_call_overhead.c:76
c23_nodiscard infix_status infix_forward_create_manual(infix_forward_t **, infix_type *, infix_type **, size_t, size_t, void *)
Generates a bound forward-call trampoline for a given function signature.
Definition trampoline.c:370
c23_nodiscard infix_status infix_type_create_struct(infix_arena_t *, infix_type **, infix_struct_member *, size_t)
Creates a new infix_type for a struct from an arena.
Definition types.c:445
infix_struct_member infix_type_create_member(const char *, infix_type *, size_t)
A factory function to create an infix_struct_member.
Definition types.c:161
@ INFIX_PRIMITIVE_DOUBLE
double
Definition infix.h:146
Recipe: Using Custom Memory Allocators
Problem: Your application uses a custom memory manager for tracking, pooling, or integration with a garbage collector. You need infix
to use your allocators instead of the standard malloc
, calloc
, etc.
Solution: infix
provides override macros (infix_malloc
, infix_free
, etc.). Define these macros before you include infix.h
to redirect all of its internal memory operations.
#include <stdio.h>
#include <stdlib.h>
static size_t g_total_allocated = 0;
void* tracking_malloc(size_t size) {
g_total_allocated += size;
printf(">> Custom Malloc: Allocating %zu bytes (Total: %zu)\n", size, g_total_allocated);
return malloc(size);
}
void tracking_free(void* ptr) {
printf(">> Custom Free\n");
free(ptr);
}
#define infix_malloc(size) tracking_malloc(size)
#define infix_free(ptr) tracking_free(ptr)
void recipe_custom_allocators() {
printf("Creating trampoline with custom allocators\n");
printf("Destroying trampoline\n");
printf("Done\n");
}
Recipe: Building a Dynamic Call Frame with an Arena
Problem: You are writing a language binding (e.g., for Python, Perl, Lua) and need to build the void* args[]
array at runtime. The arguments are coming from the host language, so you need to unbox them into temporary C values, create an array of pointers to these temporary values, and then clean everything up after the call. Doing this with malloc
for every call in a tight loop is inefficient.
Solution: Use an infix
arena to allocate memory for both the unboxed C values and the void**
array that points to them. This makes the entire call frame a single, contiguous block of memory that can be allocated and freed with extreme efficiency.
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
void process_user_data(int id, double score, const char* name) {
printf("C Function Received: ID=%d, Score=%.2f, Name='%s'\n", id, score, name);
}
void dynamic_ffi_call(
infix_forward_t* trampoline,
void* target_func,
int arg_count, ...) {
if (!call_arena) {
fprintf(stderr, "Error: Could not create call arena.\n");
return;
}
va_list va;
va_start(va, arg_count);
for (int i = 0; i < arg_count; ++i) {
if (i == 0) {
*val_ptr = va_arg(va, int);
}
else if (i == 1) {
*val_ptr = va_arg(va, double);
}
else if (i == 2) {
const char** val_ptr =
infix_arena_alloc(call_arena,
sizeof(
const char*), _Alignof(
const char*));
*val_ptr = va_arg(va, const char*);
}
}
va_end(va);
cif(target_func, NULL,
args);
}
void recipe_arena_call_frame() {
const char* signature = "(int, double, *char) -> void";
printf("Making dynamic FFI call\n");
dynamic_ffi_call(trampoline, (void*)process_user_data, 3,
123, 99.8, "test user");
}
c23_nodiscard void * infix_arena_alloc(infix_arena_t *, size_t, size_t)
Allocates a block of memory from the arena with a specific alignment.
Definition arena.c:86
How It Works & Why It's Better
- Unified Allocation: Instead of multiple calls to
malloc
(one for the args
array, one for the int
, one for the double
, etc.), all memory is sourced from a single arena.
- Performance: The allocations within the arena are extremely fast "bump" allocations, which is significantly cheaper than heap allocation, especially for many small objects.
- Simplified Cleanup: All temporary data for the call—the
void**
array and the unboxed C values—lives in the arena. A single call to infix_arena_destroy
cleans everything up instantly, eliminating the risk of memory leaks from forgetting to free
one of the many small allocations.
Advanced Optimization: Arena Resetting for Hot Loops
For a binding that needs to make many FFI calls in a tight loop, you can achieve even higher performance by creating the arena once outside the loop and "resetting" it on each iteration. Since infix_arena_t
is not an opaque type, you can do this manually:
for (int i = 0; i < 1000; ++i) {
}
size_t current_offset
The high-water mark; the offset of the next free byte.
Definition infix_internals.h:133
Chapter 9: Common Pitfalls & Troubleshooting
Recipe: Advanced Error Reporting for the Parser
Problem: A user provides an invalid signature string, and you want to give them a helpful error message indicating exactly where the syntax error occurred.
Solution: After a parsing function fails, call infix_get_last_error()
and use the position
field to point out the exact character that caused the failure.
#include <stdio.h>
void report_parse_error(const char* signature) {
fprintf(stderr, "Failed to parse signature:\n");
fprintf(stderr, " %s\n", signature);
fprintf(stderr,
" %*s^\n", (
int)err.
position,
"");
fprintf(stderr,
"Error Code: %d at position %zu\n", err.
code, err.
position);
}
else
printf("Successfully parsed signature: %s\n", signature);
}
void recipe_error_reporting() {
report_parse_error("{int, double, ^*char}");
}
infix_status status
Definition 103_unions.c:74
infix_error_details_t infix_get_last_error(void)
Retrieves detailed information about the last error that occurred on the current thread.
Definition error.c:87
infix_status
An enumeration of all possible success or failure codes from the public API.
Definition infix.h:351
A structure holding detailed information about the last error that occurred on the current thread.
Definition infix.h:1093
size_t position
For parser errors, the 0-based index in the input string where the error occurred.
Definition infix.h:1096
infix_error_code_t code
The specific error code.
Definition infix.h:1095
Mistake: Passing a Value Instead of a Pointer in <tt>args[]</tt>
- Symptom: Crash or garbage data.
- Explanation: The
args
array for a forward call must be an array of pointers to your argument values, not the values themselves.
Mistake: <tt>infix</tt> Signature Mismatch
- Symptom: Silent data corruption, garbage values, or a crash.
- Explanation: The signature string must exactly match the C type's size and alignment. A
long
is 32 bits on 64-bit Windows but 64 bits on 64-bit Linux.
- Solution: Use fixed-width types (
int32
, uint64
) whenever possible.
Pitfall: Function Pointer Syntax
- Symptom: Parser error.
- Explanation: A function type is
(...) -> ...
, and a pointer is *...
. Therefore, a pointer to a function type is *((...) -> ...)
.
- Solution:
int (*callback)(void)
becomes *(() -> int)
.
Chapter 10: A Comparative Look: <tt>infix</tt> vs. <tt>libffi</tt> and <tt>dyncall</tt>
This chapter provides a practical, code-level comparison of infix
with two other popular FFI libraries: libffi
(the industry standard) and dyncall
. All three are powerful tools, but they are built with different philosophies and trade-offs. We will compare them across three common FFI tasks.
Scenario 1: Calling a Simple Function
Goal: Call a simple function double add_doubles(double a, double b);
. This demonstrates the core calling mechanism and API ergonomics.
The <tt>dyncall</tt> Approach
dyncall
uses a "call virtual machine" (VM) where arguments are pushed one-by-one. The setup cost is incurred on every call, making it very flexible but less performant for repeated calls to the same function.
#include <dyncall.h>
DCCallVM* vm = dcNewCallVM(4096);
double result;
dcReset(vm);
dcArgDouble(vm, 1.5);
dcArgDouble(vm, 2.5);
result = dcCallDouble(vm, (DCpointer)&add_doubles);
dcFree(vm);
The <tt>libffi</tt> Approach
libffi
requires a one-time "Call Interface" (ffi_cif
) preparation. Subsequent calls are fast, but the initial type definition is manual and programmatic.
#include <ffi.h>
ffi_cif cif;
ffi_type* args_types[] = { &ffi_type_double, &ffi_type_double };
ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2,
ret_type, args_types);
double a = 1.5, b = 2.5;
void* args_values[] = { &a, &b };
double result;
ffi_call(&cif, FFI_FN(add_doubles), &result, args_values);
infix_type * ret_type
Definition 901_call_overhead.c:75
The <tt>infix</tt> Approach
infix
combines the performance model of libffi
(one-time setup) with a much higher-level, human-readable API. The key difference is the use of a simple signature string.
double a = 1.5, b = 2.5;
void*
args[] = { &a, &b };
double result;
Scenario 2: Calling a Function with a Struct
Goal: Call Point move_point(Point p);
where Point
is {double, double}
. This highlights the critical differences in type systems.
The <tt>dyncall</tt> Approach
dyncall
requires manual construction of an aggregate object (DCaggr
) to describe the struct layout. This must be done at runtime before the call.
#include <dyncall.h>
#include <dyncall_aggregate.h>
typedef struct {
double x, y; }
Point;
DCCallVM* vm = dcNewCallVM(4096);
DCaggr* ag = dcNewAggr(2);
dcAggrField(ag, DC_TYPE_DOUBLE, DC_ALIGNMENT_DOUBLE, 1);
dcAggrField(ag, DC_TYPE_DOUBLE, DC_ALIGNMENT_DOUBLE, 1);
dcCloseAggr(ag);
Point p_in = {10.0, 20.0};
dcReset(vm);
dcArgAggr(vm, ag, &p_in);
dcCallAggr(vm, (DCpointer)&
move_point, ag, &p_out);
dcFreeAggr(ag);
dcFree(vm);
The <tt>libffi</tt> Approach
libffi
also requires programmatic struct definition, which is done by creating an ffi_type
struct and an array for its elements.
#include <ffi.h>
typedef struct {
double x, y; }
Point;
ffi_type point_elements[] = { &ffi_type_double, &ffi_type_double, NULL };
ffi_type point_type;
point_type.
size = 0; point_type.alignment = 0;
point_type.type = FFI_TYPE_STRUCT;
point_type.elements = point_elements;
ffi_cif cif;
ffi_type* args_types[] = { &point_type };
ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1, &point_type, args_types);
Point p_in = {10.0, 20.0};
void* args_values[] = { &p_in };
ffi_call(&cif, FFI_FN(
move_point), &p_out, args_values);
size_t size
The total size of the type in bytes, per sizeof.
Definition infix.h:163
The <tt>infix</tt> Approach
infix
handles the entire struct definition within the signature string, making the C code for the FFI call trivial and declarative.
typedef struct {
double x, y; }
Point;
const char* signature = "({double, double}) -> {double, double}";
Point p_in = {10.0, 20.0};
void*
args[] = { &p_in };
Scenario 3: Creating a Callback
Goal: Create a native C function pointer from a custom handler to be used by qsort
.
The <tt>dyncall</tt> Approach
dyncallback
requires creating a DCCallback
object and initializing it with a C function that uses a special dcbArg*
API to retrieve arguments one by one.
#include <dyncall_callback.h>
void qsort_handler_dc(DCCallback* cb, DCArgs*
args, DCValue* result,
void* userdata) {
const int* a = (
const int*)dcbArgPointer(
args);
const int* b = (
const int*)dcbArgPointer(
args);
result->i = (*a - *b);
}
DCCallback* cb = dcbNewCallback("pp)i", &qsort_handler_dc, NULL);
qsort(numbers, 5, sizeof(int), (void*)cb);
dcbFree(cb);
The <tt>libffi</tt> Approach
libffi
can create a "closure" which is a block of executable memory that acts as the C function pointer. The handler receives arguments via ffi_call
-style arrays.
#include <ffi.h>
void qsort_handler_ffi(ffi_cif* cif,
void* ret,
void**
args,
void* userdata) {
const int* a = *(
const int**)
args;
const int* b = *(
const int**)
args;
*(ffi_sarg*)ret = (*a - *b);
}
ffi_cif cif;
ffi_type* args_types[] = { &ffi_type_pointer, &ffi_type_pointer };
ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_sint, args_types);
void* func_ptr = NULL;
ffi_closure* closure = ffi_closure_alloc(sizeof(ffi_closure), &func_ptr);
ffi_prep_closure_loc(closure, &cif, qsort_handler_ffi, NULL, func_ptr);
qsort(numbers, 5, sizeof(int), (void*)func_ptr);
ffi_closure_free(closure);
The <tt>infix</tt> Approach
infix
generates a reverse trampoline. The handler is a normal C function that receives its arguments directly, prefixed by the infix_context_t*
.
int qsort_handler_infix(
infix_context_t* ctx,
const int* a,
const int* b) {
(void)ctx;
return (*a - *b);
}
typedef int (*compare_func_t)(const void*, const void*);
qsort(numbers, 5, sizeof(int), my_comparator);
Analysis and Takeaways
Aspect | dyncall | libffi | infix |
Readability | Low (single-character signatures) | Medium (C code is clear, but type setup is verbose) | High (Human-readable, self-contained signature strings) |
Performance Model | Setup cost on every call | One-time setup (ffi_prep_cif ) | One-time setup (JIT compilation) |
Type System | Programmatic, with struct support | Manual, programmatic ffi_type creation | Integrated. Types are part of the signature string, with registry support. |
Ease of Use | Simple for primitives, complex for structs | Complex, powerful, requires deep knowledge of the API | Simple and Declarative, designed for a high-level experience. |
Callback Handler | Special API (dcbArg* ) | Generic void** arguments | Native C arguments (callback) or **Generic void** (closure). |
Chapter 11: Building Language Bindings
The Four Pillars of a Language Binding
A robust language binding built on infix
must solve four main challenges:
- Type Mapping & Signature Generation: The binding's primary job is to translate the host language's type representation (e.g., Python's
ctypes.c_int
) into an infix
signature string.
- Trampoline Caching: The binding must implement a global, persistent cache for trampolines, using the signature string as the key, to amortize the one-time JIT compilation cost.
- Memory & Lifetime Management: The binding must act as a bridge between the host language's Garbage Collector (GC) and C's manual memory management, holding references to objects to prevent premature collection.
- The Callback Bridge: A C handler must be implemented to transfer control from a native C call back into the host language's runtime, handling argument unmarshalling and potential GIL (Global Interpreter Lock) acquisition. This should use the
infix_reverse_create_closure
API.
Recipe: Porting a Python Binding from <tt>dyncall</tt> to <tt>infix</tt>
This recipe demonstrates how one might port a Python binding from a library like dyncall
to infix
.
The dyncall
approach involves a "call virtual machine" (DCCallVM*
) that arguments are pushed to one-by-one at call time. This is flexible but incurs overhead on every call.
The infix
approach shifts the expensive work (parsing and code generation) to a one-time setup phase, making subsequent calls much faster. The core logic of the binding becomes centered around a trampoline cache.
#include <Python.h>
#include <alloca.h>
static PyObject* g_trampoline_cache = NULL;
static PyObject* infix_python_call(PyObject* self, PyObject* py_args) {
PyObject* target_func_capsule = NULL;
const char* signature = NULL;
PyObject* py_func_args = NULL;
if (!PyArg_ParseTuple(py_args, "OsO!", &target_func_capsule, &signature, &PyTuple_Type, &py_func_args)) return NULL;
void* target_func = PyCapsule_GetPointer(target_func_capsule, NULL);
if(!target_func) return NULL;
if (g_trampoline_cache == NULL) g_trampoline_cache = PyDict_New();
PyObject* signature_py = PyUnicode_FromString(signature);
PyObject* capsule = PyDict_GetItem(g_trampoline_cache, signature_py);
if (capsule)
trampoline = (
infix_forward_t*)PyCapsule_GetPointer(capsule,
"infix_trampoline");
else {
PyErr_SetString(PyExc_RuntimeError, "Failed to create infix trampoline.");
Py_DECREF(signature_py);
return NULL;
}
PyDict_SetItem(g_trampoline_cache, signature_py, capsule);
Py_DECREF(capsule);
}
Py_DECREF(signature_py);
size_t num_args = PyTuple_GET_SIZE(py_func_args);
void** c_args = (void**)alloca(sizeof(void*) * num_args);
void* storage = alloca(1024);
char* storage_ptr = (char*)storage;
for (size_t i = 0; i < num_args; ++i) {
PyObject* py_arg = PyTuple_GET_ITEM(py_func_args, i);
if (PyLong_Check(py_arg)) {
long* val = (long*)storage_ptr; *val = PyLong_AsLong(py_arg);
c_args[i] = val; storage_ptr += sizeof(long);
}
else if (PyFloat_Check(py_arg)) {
double* val = (double*)storage_ptr; *val = PyFloat_AsDouble(py_arg);
c_args[i] = val; storage_ptr += sizeof(double);
}
}
cif(target_func, NULL, c_args);
Py_RETURN_NONE;
}