infix
A JIT-Powered FFI Library for C
Loading...
Searching...
No Matches
The infix FFI Cookbook

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
      • x86-64 (SSE, AVX, and AVX-512)
      • AArch64 (NEON)
      • AArch64 (Scalable Vector Extension - SVE)
    • 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 `long double`
    • 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
    • Recipe: Handling Strings and Semantic Types (`wchar_t`, etc.)
    • Recipe: Calling C++ Virtual Functions (V-Table Emulation)
    • Recipe: Bridging C++ Callbacks (`std::function`) and Lambdas
  • 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: Creating and Introspecting Semantic Aliases
    • 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: Optimizing Memory with a Shared Arena
    • 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.

// 1. Describe the signature: double atan2(double y, double x);
const char* signature = "(double, double) -> double";
// 2. Create an unbound trampoline.
infix_forward_t* trampoline = NULL;
infix_forward_create_unbound(&trampoline, signature, NULL);
// 3. Prepare arguments and invoke the call.
double y = 1.0, x = 1.0;
void* args[] = { &y, &x };
double result;
infix_forward_get_unbound_code(trampoline)((void*)atan2, &result, args);
void * args[]
Definition 202_in_structs.c:64
c23_nodiscard infix_status infix_forward_create_unbound(infix_forward_t **, const char *, infix_registry_t *)
Creates an "unbound" forward trampoline from a signature string.
Definition trampoline.c:926
c23_nodiscard infix_unbound_cif_func infix_forward_get_unbound_code(infix_forward_t *)
Gets the callable function pointer from an unbound forward trampoline.
Definition trampoline.c:282
Internal definition of a forward trampoline handle.
Definition infix_internals.h:94

‍Full example available in Ch01_SimpleCall.c.

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.

// Signature for: const char* strchr(const char* s, int c);
const char* signature = "(*char, int) -> *char";
infix_forward_t* trampoline = NULL;
infix_forward_create(&trampoline, signature, (void*)strchr, NULL);
// Prepare arguments. The value for the pointer is the address of the char* variable.
const char* haystack = "hello-world";
int needle = '-';
void* args[] = { &haystack, &needle };
const char* result_ptr = NULL;
infix_forward_get_code(trampoline)(&result_ptr, args);
c23_nodiscard infix_status infix_forward_create(infix_forward_t **, const char *, void *, infix_registry_t *)
Creates a "bound" forward trampoline from a signature string.
Definition trampoline.c:919
c23_nodiscard infix_cif_func infix_forward_get_code(infix_forward_t *)
Gets the callable function pointer from a bound forward trampoline.
Definition trampoline.c:288

‍Full example available in Ch01_Pointers.c.

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);
const char* signature = "(int, *int, *double) -> bool";
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)get_user_stats, NULL);
int user_id = 123;
// These are our "out" variables.
int post_count = 0;
double score = 0.0;
bool success;
// For "out" parameters, the args array contains pointers TO the pointer variables.
int* p_post_count = &post_count;
double* p_score = &score;
void* args[] = { &user_id, &p_post_count, &p_score };
// After the call, post_count and score are populated.
static bool get_user_stats(int user_id, int *out_posts, double *out_score)
Definition Ch01_OutParameters.c:16

‍Full example available in Ch01_OutParameters.c.

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 for the handle. For better readability and introspection, create a "semantic alias" for the handle type in a registry. This attaches a meaningful name to the *void type without changing how it's handled in the FFI call.

// 1. Use a registry to create a readable, semantic alias for the opaque handle.
infix_register_types(reg, "@FileHandle = *void;");
// 2. Create trampolines using the alias. The name @FileHandle is preserved for introspection.
infix_forward_t *t_fopen, *t_fputs, *t_fclose;
infix_forward_create(&t_fopen, "(*char, *char) -> @FileHandle", (void*)fopen, reg);
infix_forward_create(&t_fputs, "(*char, @FileHandle) -> int", (void*)fputs, reg);
infix_forward_create(&t_fclose, "(@FileHandle) -> int", (void*)fclose, reg);
// 3. Use the trampolines in sequence.
void* file_handle = NULL; // This will hold our opaque FILE*
const char* filename = "test.txt", *mode = "w";
infix_forward_get_code(t_fopen)(&file_handle, (void*[]){ &filename, &mode });
// ...
c23_nodiscard infix_status infix_register_types(infix_registry_t *, const char *)
Parses a string of type definitions and adds them to a registry.
Definition type_registry.c:460
c23_nodiscard infix_registry_t * infix_registry_create(void)
Creates a new, empty named type registry.
Definition type_registry.c:156
Internal definition of a named type registry.
Definition infix_internals.h:175

‍Full example available in Ch01_OpaquePointers.c.

</blockquote>

Chapter 2: Handling Complex Data Structures

// Common C struct used in this chapter's examples
typedef struct { double x, y; } Point;
A simple struct with two doubles, often used to test pass-by-value on registers.
Definition types.h:22

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.

// Point move_point(Point p, double dx);
const char* signature = "({double, double}, double) -> {double, double}";
infix_forward_t* trampoline = NULL;
infix_forward_create(&trampoline, signature, (void*)move_point, NULL);
Point start = { 10.0, 20.0 };
double delta_x = 5.5;
void* args[] = { &start, &delta_x };
Point move_point(Point p, double dx, double dy)
A C function to be called via a forward trampoline. Takes and returns a struct.
Definition 006_end_to_end_calls.c:35
clock_t start
Definition 901_call_overhead.c:50
clock_t end
Definition 901_call_overhead.c:50

‍Full example available in Ch02_StructByValue.c.

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);
const char* signature = "(double, double) -> {double, double}";
infix_forward_t* trampoline = NULL;
infix_forward_create(&trampoline, signature, (void*)make_point, NULL);
double x = 100.0, y = 200.0;
void* args[] = { &x, &y };
Point result;
infix_forward_get_code(trampoline)(&result, args);
static Point make_point(double x, double y)
Definition Ch02_ReturnStruct.c:19

‍Full example available in Ch02_ReturnStruct.c.

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); where LargeStruct is { int data[8]; }
const char* signature = "({[8:int]}) -> int";
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)get_first_element, NULL);
LargeStruct my_struct = { {123, -1, -1, -1, -1, -1, -1, -1} };
void* args[] = { &my_struct };
int result;
static int get_first_element(LargeStruct s)
Definition Ch02_LargeStruct.c:23
A struct larger than 16 bytes, guaranteed to be passed by reference on most ABIs.
Definition types.h:44

‍Full example available in Ch02_LargeStruct.c.

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.

// int process_packed(Packed p); where Packed is #pragma pack(1) { char a; uint64_t b; }
const char* signature = "(!{char, uint64}) -> int";
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)process_packed, NULL);
Packed p = {'X', 0x1122334455667788ULL};
int result = 0;
void* args[] = {&p};
static int process_packed(Packed p)
Definition Ch02_PackedStructs.c:25
Definition Ch02_PackedStructs.c:18

‍Full example available in Ch02_PackedStructs.c.

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.

// uint32_t process_bitfields(BitfieldStruct s);
// 1. Describe the struct by its underlying integer storage.
const char* signature = "({uint32}) -> uint32";
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)process_bitfields, NULL);
// 2. Manually pack the data into a uint32_t.
uint32_t packed_data = 0;
packed_data |= (15 & 0xF) << 0; // Field a = 15
packed_data |= (1000 & 0xFFF) << 4; // Field b = 1000
packed_data |= (30000 & 0xFFFF) << 16; // Field c = 30000
// 3. The FFI call sees a simple struct { uint32_t; }.
void* args[] = { &packed_data };
uint32_t result;
static uint32_t process_bitfields(BitfieldStruct s)
Definition Ch02_Bitfields.c:25

‍Full example available in Ch02_Bitfields.c.

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.

// int process_number_as_int(Number n); where Number is union { int i; float f; }
const char* signature = "(<int, float>) -> int";
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)process_number_as_int, NULL);
Number num_val;
num_val.i = 21;
int result = 0;
void* args[] = {&num_val};
static int process_number_as_int(Number n)
Definition Ch02_Unions.c:20
A simple union to test aggregate classification.
Definition types.h:38
int i
Definition types.h:39

‍Full example available in Ch02_Unions.c.

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);
// Note: Even if the C prototype was `arr[4]`, it decays to a pointer.
const char* signature = "(*sint64, size_t) -> sint64";
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)sum_array_elements, NULL);
int64_t my_array[] = {10, 20, 30, 40};
const int64_t* ptr_to_array = my_array;
size_t count = 4;
void* args[] = {&ptr_to_array, &count};
int64_t result = 0;
static int64_t sum_array_elements(const int64_t *arr, size_t count)
Definition Ch02_ArrayDecay.c:21

‍Full example available in Ch02_ArrayDecay.c.

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.

// 1. Define types using forward declarations for mutual recursion.
const char* definitions =
"@Employee; @Manager;" // Forward declarations
"@Manager = { name:*char, reports:[10:*@Employee] };"
"@Employee = { name:*char, manager:*@Manager };"
;
// 2. Create a trampoline using the named types.
infix_forward_t* trampoline = NULL;
infix_forward_create(&trampoline, "(*@Employee) -> *char", (void*)get_manager_name, registry);
// 3. Set up the C data and call.
Manager boss = { "Sanko", { NULL } };
Employee worker = { "Robinson", &boss };
Employee* p_worker = &worker;
const char* manager_name = NULL;
void* args[] = { &p_worker };
infix_forward_get_code(trampoline)(&manager_name, args);
const char * definitions
Definition 008_registry_introspection.c:39
infix_registry_t * registry
Definition 008_registry_introspection.c:35
static const char * get_manager_name(Employee *e)
Definition Ch02_AdvancedRegistry.c:26
Definition Ch02_AdvancedRegistry.c:20
Definition Ch02_AdvancedRegistry.c:15

‍Full example available in Ch02_AdvancedRegistry.c.

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.

// double complex c_square(double complex z);
const char* signature = "(c[double]) -> c[double]";
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)c_square, NULL);
double complex input = 3.0 + 4.0 * I;
double complex result;
void* args[] = {&input};
static double complex c_square(double complex z)
Definition Ch02_ComplexNumbers.c:21

‍Full example available in Ch02_ComplexNumbers.c.

Recipe: Working with SIMD Vectors

Problem: You need to call a high-performance C function that uses architecture-specific SIMD vector types for parallel data processing.

Solution: Use the generic v[<elements>:<type>] syntax or, where available, a convenient keyword alias (like m512d). The infix ABI logic contains the specific rules for each platform to ensure that these vectors are correctly passed in the appropriate SIMD registers (e.g., XMM/YMM/ZMM on x86-64, V/Z registers on AArch64).

This recipe is broken down by architecture, as the C types and intrinsic functions are platform-specific.


x86-64 (SSE, AVX, and AVX-512)

This example calls a function that uses AVX-512's 512-bit __m512d type to add two vectors of eight doubles each. The same principle applies to 128-bit SSE (__m128d) and 256-bit AVX (__m256d) types by simply adjusting the signature.

// __m512d vector_add_512(__m512d a, __m512d b);
// The signature "m512d" is a convenient alias for "v[8:double]".
const char* signature = "(m512d, m512d) -> m512d";
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)vector_add_512, NULL);
// Prepare arguments using AVX-512 intrinsics.
__m512d a = _mm512_set_pd(8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0);
__m512d b = _mm512_set_pd(34.0, 35.0, 36.0, 37.0, 38.0, 39.0, 40.0, 41.0);
void* args[] = {&a, &b};
__m512d result;
Definition Ch02_SIMD_AVX.c:23

‍Full example available in Ch02_SIMD_AVX.c.

</blockquote>

AArch64 (NEON)

This example calls a function that uses ARM NEON's float32x4_t type, which is a 128-bit vector of four floats.

// float neon_horizontal_sum(float32x4_t vec);
// The signature v[4:float] directly maps to float32x4_t.
const char* signature = "(v[4:float]) -> float";
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)neon_horizontal_sum, NULL);
// Prepare the NEON vector argument.
float data[] = {10.0f, 20.0f, 5.5f, 6.5f};
float32x4_t input_vec = vld1q_f32(data); // Load data into a vector register.
void* args[] = {&input_vec};
float result;

‍Full example available in Ch02_SIMD_NEON.c.

</blockquote>

AArch64 (Scalable Vector Extension - SVE)

Problem: SVE vectors do not have a fixed size; their length is determined by the CPU at runtime. How can we create a trampoline for a function that uses them?

Solution: This requires a dynamic, multi-step approach. You must first perform a runtime check for SVE support, then query the CPU's vector length, and finally build the infix signature string dynamically before creating the trampoline.

// double sve_horizontal_add(svfloat64_t vec);
// 1. Query the vector length at runtime. svcntd() gets the count of doubles.
size_t num_doubles = svcntd();
// 2. Build the signature string dynamically.
char signature[64];
snprintf(signature, sizeof(signature), "(v[%zu:double]) -> double", num_doubles);
// 3. Create the trampoline with the dynamic signature.
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)sve_horizontal_add, NULL);
// 4. Prepare arguments and call.
double* data = (double*)malloc(sizeof(double) * num_doubles);
// ... populate data ...
svfloat64_t input_vec = svld1_f64(svptrue_b64(), data);
double result;
infix_forward_get_code(t)(&result, (void*[]){&input_vec});

‍Full example available in Ch02_SIMD_SVE.c.

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.

// const char* status_to_string(StatusCode code);
// The C `enum` is based on `int`, so we describe it as `e:int`.
const char* signature = "(e:int) -> *char";
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)status_to_string, NULL);
// Pass the enum value as its underlying integer type.
int code = STATUS_ERR;
const char* result_str = NULL;
void* args[] = { &code };
infix_forward_get_code(t)(&result_str, args);
static const char * status_to_string(StatusCode code)
Definition Ch02_Enums.c:19
@ STATUS_ERR
Definition Ch02_Enums.c:17

‍Full example available in Ch02_Enums.c.

</blockquote>

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.

// 1. The handler function has a standard C signature.
int compare_integers_handler(const void* a, const void* b) {
return (*(const int*)a - *(const int*)b);
}
// 2. Create the reverse trampoline.
const char* cmp_sig = "(*void, *void) -> int";
infix_reverse_t* context = NULL;
// 3. Get the native function pointer and pass it to qsort.
typedef int (*compare_func_t)(const void*, const void*);
compare_func_t my_comparator = (compare_func_t)infix_reverse_get_code(context);
int numbers[] = { 5, 1, 4, 2, 3 };
qsort(numbers, 5, sizeof(int), my_comparator);
static int compare_integers_handler(const void *a, const void *b)
Definition Ch03_QsortCallback.c:19
c23_nodiscard infix_status infix_reverse_create_callback(infix_reverse_t **, const char *, void *, infix_registry_t *)
Creates a type-safe reverse trampoline (callback).
Definition trampoline.c:932
c23_nodiscard void * infix_reverse_get_code(const infix_reverse_t *)
Gets the native, callable C function pointer from a reverse trampoline.
Definition trampoline.c:813
Internal definition of a reverse trampoline (callback/closure) handle.
Definition infix_internals.h:121

‍Full example available in Ch03_QsortCallback.c.

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.

// 1. The generic handler retrieves state from the context's user_data field.
void my_stateful_handler(infix_context_t* context, void* ret, void** args) {
int item_value = *(int*)args[0];
ctx->sum += item_value;
}
// 2. Prepare your state and create the closure, passing a pointer to the state.
AppContext ctx = {"My List", 0};
infix_reverse_t* rt = NULL;
infix_reverse_create_closure(&rt, "(int) -> void", my_stateful_handler, &ctx, NULL);
// 3. Use the generated callback with a C library function.
int list[] = {10, 20, 30};
process_list(list, 3, processor_ptr);
static void my_stateful_handler(infix_context_t *context, void *ret, void **args)
Definition Ch03_StatefulCallback.c:25
void(* item_processor_t)(int)
Definition Ch03_StatefulCallback.c:39
static void process_list(const int *items, int count, item_processor_t process_func)
Definition Ch03_StatefulCallback.c:40
c23_nodiscard infix_status infix_reverse_create_closure(infix_reverse_t **, const char *, infix_closure_handler_fn, void *, infix_registry_t *)
Creates a generic reverse trampoline (closure) for stateful callbacks.
Definition trampoline.c:962
c23_nodiscard void * infix_reverse_get_user_data(const infix_reverse_t *)
Gets the user-provided data pointer from a closure context.
Definition trampoline.c:824
Definition Ch03_StatefulCallback.c:18
int sum
Definition Ch03_StatefulCallback.c:20

‍Full example available in Ch03_StatefulCallback.c.

</blockquote>

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.

// Signature for: printf(const char* format, int arg1, double arg2);
const char* signature = "(*char; int, double) -> int";
infix_forward_t* trampoline = NULL;
infix_forward_create(&trampoline, signature, (void*)printf, NULL);
const char* fmt = "Count: %d, Value: %.2f\n";
int count = 42;
double value = 123.45;
void* args[] = { &fmt, &count, &value };
int result;
infix_forward_get_code(trampoline)(&result, args);

‍Full example available in Ch04_VariadicPrintf.c.

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.

// 1. Create the inner callback.
int multiply_handler(int x) { return x * 10; }
infix_reverse_t* inner_cb_ctx = NULL;
infix_reverse_create_callback(&inner_cb_ctx, "(int)->int", (void*)multiply_handler, NULL);
// 2. Create the outer trampoline for the function that *takes* the callback.
// Signature for: int harness_func( int(*)(int), int );
const char* harness_sig = "(*((int)->int), int) -> int";
infix_forward_t* harness_trampoline = NULL;
infix_forward_create(&harness_trampoline, harness_sig, (void*)harness_func, NULL);
// 3. Get the native pointer from the callback and pass it as an argument.
void* inner_cb_ptr = infix_reverse_get_code(inner_cb_ctx);
int value = 7;
void* harness_args[] = { &inner_cb_ptr, &value };
int result;
infix_forward_get_code(harness_trampoline)(&result, harness_args);
static int multiply_handler(int x)
Definition Ch04_CallbackAsArg.c:16
static int harness_func(int(*worker_func)(int), int base_val)
Definition Ch04_CallbackAsArg.c:22

‍Full example available in Ch04_CallbackAsArg.c.

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.

// 1. Define types for the object, function pointers, and v-table.
"@Adder = { val: int };"
"@Adder_add_fn = (*@Adder, int) -> int;"
"@AdderVTable = { add: *@Adder_add_fn };"
);
// 2. Create an object and get a pointer to its v-table.
Adder* my_adder = create_adder(100);
const AdderVTable* vtable = &VTABLE;
// 3. Read the function pointer from the v-table.
void* add_func_ptr = (void*)vtable->add;
// 4. Create a trampoline for the specific function pointer and call it.
infix_forward_t* t_add = NULL;
infix_forward_create(&t_add, "@Adder_add_fn", add_func_ptr, reg);
int amount_to_add = 23, result;
void* add_args[] = { &my_adder, &amount_to_add };
infix_forward_get_code(t_add)(&result, add_args);
const AdderVTable VTABLE
Definition Ch04_VTableCStyle.c:44
static Adder * create_adder(int base)
Definition Ch04_VTableCStyle.c:47
Definition Ch04_VTableCStyle.c:38
int(* add)(Adder *self, int amount)
Definition Ch04_VTableCStyle.c:39
Definition Ch04_VTableCStyle.c:19

‍Full example available in Ch04_VTableCStyle.c.

Recipe: Handling <tt>long double</tt>

Problem: You need to call a function that uses long double, 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.

// long double native_sqrtl(long double x);
const char* signature = "(longdouble) -> longdouble";
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)native_sqrtl, NULL);
long double input = 144.0L;
long double result = 0.0L;
void* args[] = { &input };
static long double native_sqrtl(long double x)
Definition Ch04_LongDouble.c:21

‍Full example available in Ch04_LongDouble.c.

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.

// 1. Create the innermost forward trampoline (for `multiply`).
infix_forward_t* fwd_multiply = NULL;
infix_forward_create(&fwd_multiply, "(int, int)->int", (void*)multiply, NULL);
// 2. Create the reverse closure, passing the forward trampoline as user_data.
// The handler will use this trampoline to make the nested call.
infix_reverse_t* rev_nested = NULL;
infix_reverse_create_closure(&rev_nested, "(int)->int", nested_call_handler, fwd_multiply, NULL);
// 3. Create the outermost forward trampoline (for `harness`).
infix_forward_t* fwd_harness = NULL;
const char* harness_sig = "(*((int)->int), int)->int";
infix_forward_create(&fwd_harness, harness_sig, (void*)harness, NULL);
// 4. Execute the call chain.
void* callback_ptr = infix_reverse_get_code(rev_nested);
int base_val = 8;
void* harness_args[] = { &callback_ptr, &base_val };
int final_result;
infix_forward_get_code(fwd_harness)(&final_result, harness_args);
void harness(int(*func_ptr)(int, int), int a, int b, int expected)
Definition 304_reverse_call_types.c:33
static void nested_call_handler(infix_context_t *ctx, void *ret, void **args)
Definition Ch04_Reentrancy.c:29
static int multiply(int a, int b)
Definition Ch04_Reentrancy.c:22

‍Full example available in Ch04_Reentrancy.c.

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.

// Main thread: Create the trampoline.
infix_forward_t* trampoline = NULL;
infix_forward_create(&trampoline, "(int, int)->int", (void*)add, NULL);
// Main thread: Prepare data for the worker thread, including the callable pointer.
thread_data_t data = { infix_forward_get_code(trampoline), 0 };
// Main thread: Spawn a worker thread.
pthread_t thread_id;
pthread_create(&thread_id, NULL, worker_thread_func, &data);
// Worker thread (`worker_thread_func`):
// ...
// data->cif(&data->result, args); // <-- FFI call happens here
// ...
pthread_join(thread_id, NULL);
static int add(int a, int b)
Definition Ch04_ThreadSafety.c:25
void * worker_thread_func(void *arg)
Definition Ch04_ThreadSafety.c:37
Definition Ch04_ThreadSafety.c:28

‍Full example available in Ch04_ThreadSafety.c.

</blockquote>

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.

// Mangled names depend on the compiler. This example gets them from a helper.
const char* mangled_ctor = get_mangled_constructor();
const char* mangled_getvalue = get_mangled_getvalue();
// Constructor is: void MyClass(MyClass* this, int val);
infix_forward_create(&t_ctor, "(*void, int)->void", p_ctor, NULL);
// Method is: int getValue(const MyClass* this);
infix_forward_create(&t_getval, "(*void)->int", p_getval, NULL);
// --- Simulate `MyClass* obj = new MyClass(100);` ---
void* obj = malloc(obj_size);
int initial_val = 100;
infix_forward_get_code(t_ctor)(NULL, (void*[]){ &obj, &initial_val });
// --- Simulate `int result = obj->getValue();` ---
int result;
infix_forward_get_code(t_getval)(&result, (void*[]){ &obj });

‍Full example available in Ch05_CppMangledNames.c and library source in libs/MyClass.cpp.

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.

// Mangled name for `Box<double>::get_value()` on GCC/Clang
const char* MANGLED_GET_DBL = "_ZNK3BoxIdE9get_valueEv";
// In a real scenario, you would call the mangled constructor.
// For simplicity here, we manually create the object layout.
double val = 3.14;
void* my_box = malloc(sizeof(double));
memcpy(my_box, &val, sizeof(double));
void* p_get_value = infix_library_get_symbol(lib, MANGLED_GET_DBL);
// Signature: double get_value(const Box<double>* this)
infix_forward_t* t_get = NULL;
infix_forward_create(&t_get, "(*void) -> double", p_get_value, NULL);
double result;
infix_forward_get_code(t_get)(&result, (void*[]){ &my_box });
const char * MANGLED_GET_DBL
Definition Ch05_CppTemplates.c:15
c23_nodiscard void * infix_library_get_symbol(infix_library_t *, const char *)
Retrieves the address of a symbol (function or variable) from a loaded library.
Definition loader.c:121

‍Full example available in Ch05_CppTemplates.c and library source in libs/Box.cpp.

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.

// libd_math.d
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.

! libfortran_math.f90
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

Recipe: Handling Strings and Semantic Types (<tt>wchar_t</tt>, etc.)

Problem: You are building a language binding and need to introspect a type to marshal data correctly. A signature like *uint16 is structurally correct for a Windows wchar_t*, but how does your code know it's a null-terminated string and not just a pointer to an integer?

Solution: Use the infix type registry to create semantic aliases. The new infix_type_get_name() API allows you to retrieve this semantic name at runtime, giving your wrapper the context it needs to perform the correct marshalling logic. This pattern provides the missing link between C's structural type system and the semantic needs of a high-level language.

Let's model the Windows MessageBoxW function, which takes two UTF-16 strings.

// 1. Define semantic aliases in a registry.
const char* type_defs =
"@HWND = *void;"
"@UTF16String = *uint16;" // Represents wchar_t* on Windows
"@UTF8String = *char;"; // Represents const char*
// 2. Define function signatures using these aliases.
const char* signature = "(@HWND, @UTF16String, @UTF16String, uint) -> int";
// 3. In your binding, introspect the type to make marshalling decisions.
const infix_type* arg_type = infix_forward_get_arg_type(type, 1);
const char* semantic_name = infix_type_get_name(arg_type); // Will be "UTF16String"
if (semantic_name && strcmp(semantic_name, "UTF16String") == 0) {
// This is our cue! Marshal the user's string as UTF-16.
}
infix_arena_t * arena
Definition 005_layouts.c:68
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:1026
c23_nodiscard const char * infix_type_get_name(const infix_type *)
Gets the semantic alias of a type, if one exists.
Definition types.c:1100
c23_nodiscard const infix_type * infix_forward_get_arg_type(const infix_forward_t *, size_t)
Gets the type of a specific argument for a forward trampoline.
Definition types.c:1214
A semi-opaque structure that describes a C type.
Definition infix.h:211

‍Full example available in Ch05_SemanticStrings.c.

Recipe: Calling C++ Virtual Functions (V-Table Emulation)

Problem: You have a pointer to a C++ polymorphic base class object (e.g., Shape*) and you need to call a virtual function on it from C, achieving true dynamic dispatch.

Solution: Emulate what the C++ compiler does: manually read the object's v-table pointer (vptr), find the function pointer at the correct index within the v-table, and use infix to call it.

// 1. Create a C++ object via a factory function.
void* rect_obj = create_rectangle(10.0, 5.0);
// 2. Manually read the v-table pointer from the object's memory.
void** vptr = (void**)rect_obj;
void** vtable = *vptr;
// 3. Read function pointers from their known indices in the v-table.
void* area_fn_ptr = vtable[0]; // double area() const -> index 0
void* name_fn_ptr = vtable[1]; // const char* name() const -> index 1
// 4. Create trampolines for the discovered function pointers.
infix_forward_create(&t_area, "(*void)->double", area_fn_ptr, NULL);
infix_forward_create(&t_name, "(*void)->*char", name_fn_ptr, NULL);
// 5. Call the virtual functions, passing the object as the `this` pointer.
double rect_area;
infix_forward_get_code(t_area)(&rect_area, (void*[]){ &rect_obj });

‍Full example available in Ch05_CppVirtualFunctions.c and library source in libs/shapes.cpp.

Recipe: Bridging C++ Callbacks (<tt>std::function</tt>) and Lambdas

Problem: You need to call a C++ method that accepts a callback (e.g., std::function<void(int)>), and provide a stateful handler from your C application.

Solution: This is a powerful, two-way FFI interaction. You will create an infix closure to represent your C-side state, and then call a mangled C++ method to register that closure's components (its C function pointer and its state pointer) with the C++ object.

// 1. Create the C-side state and the infix closure.
C_AppState app_state = {0};
infix_reverse_t* closure = NULL;
infix_reverse_create_closure(&closure, "(int, *void)->void", my_closure_handler, &app_state, NULL);
// 2. Create an instance of the C++ EventManager object.
void* manager_obj = malloc(get_size());
infix_forward_create(&t_ctor, "(*void)->void", p_ctor, NULL);
infix_forward_get_code(t_ctor)(NULL, &manager_obj);
// 3. Call the C++ `set_handler` method to register our closure's components.
const char* sig = "(*void, *((int, *void)->void), *void)->void";
infix_forward_create(&t_set_handler, sig, p_set_handler, NULL);
void* closure_c_func = infix_reverse_get_code(closure);
void* closure_user_data = infix_reverse_get_user_data(closure);
void* set_handler_args[] = { &manager_obj, &closure_c_func, &closure_user_data };
infix_forward_get_code(t_set_handler)(NULL, set_handler_args);
// 4. Call the C++ `trigger` method to make it invoke our C callback.
infix_forward_create(&t_trigger, "(*void, int)->void", p_trigger, NULL);
// ...

‍Full example available in Ch05_CppCallbacks.cpp and library source in libs/EventManager.cpp.

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.

// 1. Open the system library by name.
infix_library_t* user32 = infix_library_open("user32.dll");
// 2. Look up the address of the `MessageBoxA` function.
void* pMessageBoxA = infix_library_get_symbol(user32, "MessageBoxA");
// 3. Define the signature and create the trampoline.
// int MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
const char* sig = "(*void, *char, *char, uint32) -> int";
infix_forward_t* t = NULL;
infix_forward_create(&t, sig, pMessageBoxA, NULL);
// 4. Prepare arguments and call the function.
void* hwnd = NULL;
const char* text = "Hello from a dynamically loaded function!";
// ...
int result;
c23_nodiscard infix_library_t * infix_library_open(const char *)
Opens a dynamic library and returns a handle to it.
Definition loader.c:50
Internal definition of a dynamic library handle.
Definition infix_internals.h:218

‍Full example available in Ch06_SystemLibraries.c.

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

infix_library_t* lib = infix_library_open("./libglobals.so");
int counter_val = 0;
// 1. Read the initial value. The signature is simply the type of the variable.
infix_read_global(lib, "global_counter", "int", &counter_val, NULL);
// 2. Write a new value to the global variable.
int new_val = 100;
infix_write_global(lib, "global_counter", "int", &new_val, NULL);
// 3. Read it back to confirm.
infix_read_global(lib, "global_counter", "int", &counter_val, NULL);
c23_nodiscard infix_status infix_write_global(infix_library_t *, const char *, const char *, void *, infix_registry_t *)
Writes data from a buffer into a global variable in a library.
Definition loader.c:196
c23_nodiscard infix_status infix_read_global(infix_library_t *, const char *, const char *, void *, infix_registry_t *)
Reads the value of a global variable from a library into a buffer.
Definition loader.c:149

Example 2: Aggregate (Struct) Variable

infix_register_types(reg, "@Config = {*char, int};");
Config local_config;
// 1. Read the global struct into our local variable.
infix_read_global(lib, "g_config", "@Config", &local_config, reg);
// 2. Modify and write the struct back to the library.
Config new_config = { "updated", 2 };
infix_write_global(lib, "g_config", "@Config", &new_config, reg);
Definition Ch06_GlobalVariables.c:15

‍Full example available in Ch06_GlobalVariables.c and library source in libs/libglobals.c.

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.

// We only need to open libA. The OS will handle loading libB.
infix_library_t* lib = infix_library_open("./libA.so");
void* p_entry = infix_library_get_symbol(lib, "entry_point_a");
infix_forward_t* t = NULL;
infix_forward_create(&t, "()->int", p_entry, NULL);
int result;
infix_forward_get_code(t)(&result, NULL);

‍Full example available in Ch06_LibraryDependencies.c and library sources in libs/libA.c and libs/libB.c.

</blockquote>

Chapter 7: Introspection for Data Marshalling

Recipe: Creating and Introspecting Semantic Aliases

Problem: You need to distinguish between types that are structurally identical (like multiple kinds of void* handles) for your language binding's marshalling logic.

Solution: Use the type registry to create semantic aliases. The infix_type_get_name() function allows you to retrieve these names at runtime, providing the context your code needs.

// 1. Define semantic aliases for different handle types.
"@DatabaseHandle = *void;"
"@IteratorHandle = *void;"
"@MyInt = int32;"
);
// 2. In your binding, parse a type and check its semantic name.
infix_type* type = NULL;
infix_type_from_signature(&type, &arena, "@DatabaseHandle", registry);
const char* name = infix_type_get_name(type); // name will be "DatabaseHandle"
if (name && strcmp(name, "DatabaseHandle") == 0) {
// Correctly identified! Now perform database-specific marshalling.
}
// The underlying structural type is still fully accessible.
// ...
}
c23_nodiscard infix_type_category infix_type_get_category(const infix_type *)
Gets the fundamental category of a type.
Definition types.c:1111
@ INFIX_TYPE_POINTER
Definition infix.h:164
Internal definition of a memory arena.
Definition infix_internals.h:146

‍Full example available in Ch07_SemanticAliases.c.

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.

void marshal_ordered_data(void* dest, const char* sig, void** src) {
// 1. Parse the signature to get the struct's layout information.
infix_type* type = NULL;
infix_type_from_signature(&type, &arena, sig, NULL);
// 2. Iterate through the struct's members.
for (size_t i = 0; i < infix_type_get_member_count(type); ++i) {
const infix_struct_member* member = infix_type_get_member(type, i);
// 3. Use the offset and size to copy data into the correct location.
memcpy((char*)dest + member->offset, src[i], infix_type_get_size(member->type));
}
}
static void marshal_ordered_data(void *dest, const char *sig, void **src)
A generic marshalling function that packs data into a struct.
Definition Ch07_DynamicMarshalling.c:33
infix_type * type
Definition infix.h:279
size_t offset
Definition infix.h:280
void infix_arena_destroy(infix_arena_t *)
Destroys an arena and frees all memory allocated from it.
Definition arena.c:90
c23_nodiscard size_t infix_type_get_size(const infix_type *)
Gets the size of a type in bytes.
Definition types.c:1120
c23_nodiscard size_t infix_type_get_member_count(const infix_type *)
Gets the number of members in a struct or union type.
Definition types.c:1135
c23_nodiscard const infix_struct_member * infix_type_get_member(const infix_type *, size_t)
Gets a specific member from a struct or union type.
Definition types.c:1147
Describes a single member of a C struct or union.
Definition infix.h:277

‍Full example available in Ch07_DynamicMarshalling.c.

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.

// Imagine this data comes from a config file
const char* user_defined_fields[] = { "int", "int", "double" };
int num_fields = 3;
char signature_buffer = "{";
// 1. Build the signature string dynamically.
for (int i = 0; i < num_fields; ++i) {
strcat(signature_buffer, user_defined_fields[i]);
if (i < num_fields - 1) strcat(signature_buffer, ",");
}
strcat(signature_buffer, "}"); // Final string is "{int,int,double}"
// 2. Use the dynamic signature to get layout information.
infix_type* dynamic_type = NULL;
infix_type_from_signature(&dynamic_type, &arena, signature_buffer, NULL);
// ...

‍Full example available in Ch07_DynamicSignatures.c.

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) {
// 1. Introspect the trampoline to get expected argument count.
size_t num_expected_args = infix_forward_get_num_args(trampoline);
if (num_provided_args != num_expected_args) {
fprintf(stderr, "Error: Incorrect number of arguments...\n");
return;
}
// A real binding would also check the types using infix_forward_get_arg_type().
// 2. Make the call.
cif(target_func, NULL, args);
}
static bool dynamic_wrapper(infix_forward_t *trampoline, void *target_func, void **args, size_t num_provided_args, void *ret_buffer)
A conceptual generic "wrapper" function for an unbound trampoline.
Definition Ch07_IntrospectWrapper.c:29
void(* infix_unbound_cif_func)(void *, void *, void **)
A function pointer type for an unbound forward trampoline.
Definition infix.h:361
c23_nodiscard size_t infix_forward_get_num_args(const infix_forward_t *)
Gets the total number of arguments for a forward trampoline.
Definition types.c:1186

‍Full example available in Ch07_IntrospectWrapper.c.

</blockquote>

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.

// FAST: Create once, call many times
infix_forward_t* t = NULL;
infix_forward_create(&t, "(int, int) -> int", my_func, NULL);
int result;
void* args[] = { /* ... */ };
for (int i = 0; i < 1000000; ++i) {
cif(&result, args); // VERY FAST
}
void(* infix_cif_func)(void *, void **)
A function pointer type for a bound forward trampoline.
Definition infix.h:371
void infix_forward_destroy(infix_forward_t *)
Destroys a forward trampoline and frees all associated memory.
Definition trampoline.c:523

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() {
infix_type* int_array_type = NULL;
infix_type_create_array(arena, &int_array_type, int_type, 100);
// ... use these types with `infix_forward_create_manual` ...
// A single call to destroy the arena cleans up everything allocated from it.
}
c23_nodiscard infix_arena_t * infix_arena_create(size_t)
Creates a new memory arena.
Definition arena.c:55
c23_nodiscard infix_status infix_type_create_array(infix_arena_t *, infix_type **, infix_type *, size_t)
Creates a new fixed-size array type.
Definition types.c:299
c23_nodiscard infix_type * infix_type_create_primitive(infix_primitive_type_id)
Creates a static descriptor for a primitive C type.
Definition types.c:138
@ INFIX_PRIMITIVE_SINT32
Definition infix.h:186

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.

// 1. Create an arena to hold all our type definitions.
// 2. Manually define the 'Point' struct type.
infix_struct_member point_members[] = {
};
infix_type* point_type = NULL;
infix_type_create_struct(arena, &point_type, point_members, 2);
// 3. Define the argument types for the function.
// 4. Create the trampoline using the manually created types.
infix_forward_t* trampoline = NULL;
infix_forward_create_manual(&trampoline, point_type, arg_types, 2, 2, (void*)move_point);
infix_type * arg_types[]
Definition 901_call_overhead.c:67
c23_nodiscard infix_status infix_forward_create_manual(infix_forward_t **, infix_type *, infix_type **, size_t, size_t, void *)
Creates a bound forward trampoline from infix_type objects.
Definition trampoline.c:479
c23_nodiscard infix_status infix_type_create_struct(infix_arena_t *, infix_type **, infix_struct_member *, size_t)
Creates a new struct type from an array of members, calculating layout automatically.
Definition types.c:490
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:202
@ INFIX_PRIMITIVE_DOUBLE
Definition infix.h:192

‍Full example available in Ch08_ManualAPI.c.

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.

// 1. Define your custom memory management functions.
static void* tracking_malloc(size_t size) { /* ... */ }
static void tracking_free(void* ptr) { /* ... */ }
// 2. Define the infix override macros BEFORE including infix.h
#define infix_malloc(size) tracking_malloc(size)
#define infix_free(ptr) tracking_free(ptr)
#include <infix/infix.h>
// Now, all calls like infix_forward_create will use your allocators.
infix_forward_create(&trampoline, "()->void", (void*)dummy_func, NULL);
void dummy_func()
Definition 851_lifecycle_regression.c:35
void * tracking_malloc(size_t size)
Definition Ch08_CustomAllocators.c:20
void tracking_free(void *ptr)
Definition Ch08_CustomAllocators.c:28
The public interface for the infix FFI library.

‍Full example available in Ch08_CustomAllocators.c.

Recipe: Optimizing Memory with a Shared Arena

Problem: Your application creates a large number of trampolines that all reference the same set of complex, named types (e.g., @Point, @User). By default, infix deep-copies the metadata for these types into each trampoline's private memory, leading to high memory consumption and slower creation times.

Solution: Use the shared arena pattern. By creating the type registry and all related trampolines within a single, user-managed arena, you instruct infix to share pointers to the canonical named types instead of copying them. This drastically reduces memory usage and speeds up trampoline creation, but it requires you to manage the lifetime of the shared arena carefully.

When to use it: This is an advanced pattern ideal for language runtimes, plugin systems, or long-running applications that create many FFI interfaces referencing a common set of C headers.

#include <infix/infix.h>
#include <stdio.h>
#include <stdint.h>
// Dummy C types and functions to interact with
typedef struct { double x; double y; } Point;
typedef struct { uint64_t id; const char* name; } User;
void handle_point(const Point* p) { /* ... */ }
void handle_user(const User* u) { /* ... */ }
void shared_arena_example() {
// Create a single, long-lived arena to hold everything.
infix_arena_t* shared_arena = infix_arena_create(65536);
// Create the type registry *within* the shared arena.
const char* my_types =
"@Point = { x: double, y: double };"
"@User = { id: uint64, name: *char };";
// Create multiple trampolines, also telling them to use the shared arena.
// Because they share an arena with the registry, the metadata for @Point and
// @User will be shared via pointers, not deep-copied.
infix_forward_t *t_point = NULL, *t_user = NULL;
infix_forward_create_in_arena(&t_point, shared_arena, "(*@Point)->void", (void*)handle_point, registry);
infix_forward_create_in_arena(&t_user, shared_arena, "(*@User)->void", (void*)handle_user, registry);
// ... use the trampolines ...
// The user is responsible for the lifetime of all objects. Destroying the
// handles and registry first is good practice before freeing the master arena.
infix_arena_destroy(shared_arena);
}
void handle_point(const Point *p)
Definition Ch08_SharedArena.c:34
void handle_user(const User *u)
Definition Ch08_SharedArena.c:41
c23_nodiscard infix_status infix_forward_create_in_arena(infix_forward_t **, infix_arena_t *, const char *, void *, infix_registry_t *)
Creates a "bound" forward trampoline within a user-provided arena.
Definition trampoline.c:832
void infix_registry_destroy(infix_registry_t *)
Destroys a type registry and frees all associated memory.
Definition type_registry.c:230
c23_nodiscard infix_registry_t * infix_registry_create_in_arena(infix_arena_t *arena)
Creates a new named type registry that allocates from a user-provided arena.
Definition type_registry.c:196
Definition Ch08_SharedArena.c:29

‍Full, runnable example available in Ch08_SharedArena.c.

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.

void dynamic_ffi_call(infix_forward_t* trampoline, ...) {
// 1. Create a temporary arena for this call's entire data frame.
infix_arena_t* call_arena = infix_arena_create(4096);
// 2. Allocate the void** array itself from the arena.
void** args = infix_arena_alloc(call_arena, sizeof(void*) * arg_count, _Alignof(void*));
// 3. For each argument, allocate space for its C value in the arena and set the pointer.
for (int i = 0; i < arg_count; ++i) {
int* val_ptr = infix_arena_alloc(call_arena, sizeof(int), _Alignof(int));
*val_ptr = va_arg(va, int); // Get value from dynamic source
args[i] = val_ptr;
}
// 4. Make the FFI call.
infix_forward_get_unbound_code(trampoline)(target_func, NULL, args);
// 5. A single free cleans up the void** array AND all the argument values.
infix_arena_destroy(call_arena);
}
static void dynamic_ffi_call(infix_forward_t *trampoline, void *target_func, int arg_count,...)
Definition Ch08_ArenaCallFrame.c:31
c23_nodiscard void * infix_arena_alloc(infix_arena_t *, size_t, size_t)
Allocates a block of memory from an arena.
Definition arena.c:126

‍Full example available in Ch08_ArenaCallFrame.c.

How It Works & Why It's Better

  1. 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.
  2. Performance: The allocations within the arena are extremely fast "bump" allocations, which is significantly cheaper than heap allocation, especially for many small objects.
  3. 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:

// Inside a hypothetical binding's loop...
infix_arena_t* loop_arena = infix_arena_create(65536);
for (int i = 0; i < 1000; ++i) {
// Before building the args, save the current allocation point.
size_t arena_state = loop_arena->current_offset;
// ... build the void** args and argument values from the arena ...
// ... make the FFI call ...
// Instead of destroying the arena, just reset the offset.
// This is virtually free and avoids all allocation/deallocation overhead inside the loop.
loop_arena->current_offset = arena_state;
}
infix_arena_destroy(loop_arena);
size_t current_offset
Definition infix_internals.h:149

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, code, and message fields to generate a detailed diagnostic.

void report_parse_error(const char* signature) {
infix_type* type = NULL;
infix_status status = infix_type_from_signature(&type, &arena, signature, NULL);
fprintf(stderr, "Failed to parse signature:\n");
fprintf(stderr, " %s\n", signature);
// Print a caret '^' pointing to the error location.
fprintf(stderr, " %*s^\n", (int)err.position, "");
fprintf(stderr, "Error: %s (code: %d, position: %zu)\n",
err.message, err.code, err.position);
}
}
// This signature has an invalid character '^' instead of a comma.
report_parse_error("{int, double, ^*char}");
infix_status status
Definition 103_unions.c:66
static void report_parse_error(const char *signature)
A helper function that attempts to parse a signature and reports detailed error information on failur...
Definition Ch09_ErrorReporting.c:21
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:270
size_t position
Definition infix.h:1363
char message[256]
Definition infix.h:1365
infix_status
Enumerates the possible status codes returned by infix API functions.
Definition infix.h:389
infix_error_code_t code
Definition infix.h:1362
@ INFIX_SUCCESS
Definition infix.h:390
Provides detailed, thread-local information about the last error that occurred.
Definition infix.h:1360

‍Full example available in Ch09_ErrorReporting.c.

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;
// Per-call setup and execution
dcReset(vm);
dcArgDouble(vm, 1.5);
dcArgDouble(vm, 2.5);
result = dcCallDouble(vm, (DCpointer)&add_doubles); // result is 4.0
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_type* ret_type = &ffi_type_double;
// One-time setup
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;
// Subsequent calls are fast
ffi_call(&cif, FFI_FN(add_doubles), &result, args_values);
infix_type * ret_type
Definition 901_call_overhead.c:66

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.

#include <infix/infix.h>
// ...
infix_forward_t* t = NULL;
// One-time setup from a simple string
infix_forward_create(&t, "(double, double) -> double", (void*)add_doubles, NULL);
double a = 1.5, b = 2.5;
void* args[] = { &a, &b };
double result;
// Subsequent calls are very fast
cif(&result, args);

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);
// 1. Manually describe the struct layout for dyncall
DCaggr* ag = dcNewAggr(2); // 2 members
dcAggrField(ag, DC_TYPE_DOUBLE, DC_ALIGNMENT_DOUBLE, 1); // member x
dcAggrField(ag, DC_TYPE_DOUBLE, DC_ALIGNMENT_DOUBLE, 1); // member y
dcCloseAggr(ag);
// 2. Prepare the struct data and call
Point p_in = {10.0, 20.0};
Point p_out;
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;
// 1. Manually define the struct layout for libffi
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;
// 2. Prepare the CIF using the new struct type
ffi_cif cif;
ffi_type* args_types[] = { &point_type };
ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1, &point_type, args_types);
// 3. Prepare args and call
Point p_in = {10.0, 20.0};
Point p_out;
void* args_values[] = { &p_in };
ffi_call(&cif, FFI_FN(move_point), &p_out, args_values);
size_t size
Definition infix.h:214

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.

#include <infix/infix.h>
// ...
typedef struct { double x, y; } Point;
// 1. Describe the struct and function in one line.
const char* signature = "({double, double}) -> {double, double}";
infix_forward_t* t = NULL;
infix_forward_create(&t, signature, (void*)move_point, NULL);
// 2. Prepare args and call.
Point p_in = {10.0, 20.0};
Point p_out;
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>
// 1. The handler uses the dyncallback API to get arguments.
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);
}
// 2. Create the callback object.
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>
// 1. The handler receives arguments in libffi's generic format.
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);
}
// 2. Prepare the CIF for the callback's signature.
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);
// 3. Allocate and create the closure.
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*.

#include <infix/infix.h>
// 1. The handler is a standard C function with the context as the first argument.
int qsort_handler_infix(const void* a, const void* b) {
return (*(const int*)a - *(const int*)b);
}
// 2. Create the reverse trampoline from a signature.
infix_reverse_t* context = NULL;
infix_reverse_create_callback(&context, "(*void, *void)->int", (void*)qsort_handler_infix, NULL);
// 3. Get the native function pointer and use it.
typedef int (*compare_func_t)(const void*, const void*);
compare_func_t my_comparator = (compare_func_t)infix_reverse_get_code(context);
qsort(numbers, 5, sizeof(int), my_comparator);
void infix_reverse_destroy(infix_reverse_t *)
Destroys a reverse trampoline and frees all associated memory.
Definition trampoline.c:794

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

// Conceptual port to infix for a Python module
#include <Python.h>
#include <infix/infix.h>
#include <alloca.h> // For alloca
// A global Python dictionary to cache trampolines: { signature_str: PyCapsule(trampoline) }
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();
// 1. Trampoline Caching
PyObject* signature_py = PyUnicode_FromString(signature);
PyObject* capsule = PyDict_GetItem(g_trampoline_cache, signature_py);
infix_forward_t* trampoline = NULL;
if (capsule)
trampoline = (infix_forward_t*)PyCapsule_GetPointer(capsule, "infix_trampoline");
else {
// Not in cache: create, then store in cache via a PyCapsule.
if (infix_forward_create_unbound(&trampoline, signature, NULL) != INFIX_SUCCESS) {
PyErr_SetString(PyExc_RuntimeError, "Failed to create infix trampoline.");
Py_DECREF(signature_py);
return NULL;
}
capsule = PyCapsule_New(trampoline, "infix_trampoline", (PyCapsule_Destructor)infix_forward_destroy);
PyDict_SetItem(g_trampoline_cache, signature_py, capsule);
Py_DECREF(capsule);
}
Py_DECREF(signature_py);
// 2. Argument Marshalling (simplified)
size_t num_args = PyTuple_GET_SIZE(py_func_args);
void** c_args = (void**)alloca(sizeof(void*) * num_args);
// In a real binding, this storage would need to be managed more robustly.
// Using an infix_arena_t here is the recommended production approach.
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);
} // ... etc.
}
// 3. The FFI Call
// A real binding would inspect the signature to handle the return value.
cif(target_func, NULL, c_args);
Py_RETURN_NONE;
}