|
infix
A JIT-Powered FFI Library for C
|
This guide outlines the steps required to add support for a new CPU architecture or Application Binary Interface (ABI) to the infix FFI library. The library is designed to be highly portable, with a clean separation between platform-agnostic logic and ABI-specific implementations.
We will use RISC-V 64-bit (RV64GC) with the standard LP64D ABI as a practical example throughout this guide.
This is the most critical step. Before writing any code, you must have a solid understanding of the target ABI specification. For RISC-V, this means studying the official calling convention document.
You need to answer these key questions:
a0 through a7 (also known as x10 through x17).float and double?fa0 through fa7 (also known as f10 through f17).a0 and a1 for integers/pointers up to 128 bits. fa0 and fa1 for floats/doubles.a...) or FPRs (fa...).a0.s0/fp), the return address (ra), and the saved registers s1 through s11.The first code change is to teach the library how to recognize the new platform at compile time. Open src/common/infix_config.h.
Add Architecture Macro: Locate the processor architecture detection block and add a new #define INFIX_ARCH_* macro. The standard compiler macro for RISC-V is __riscv, and the bit width is checked with __riscv_xlen.
c // In src/common/infix_config.h, after the other architecture checks... #elif defined(__riscv) && __riscv_xlen == 64 #define INFIX_ARCH_RISCV64 #else #error "Unsupported architecture." #endif
Add ABI Macro: In the "Target ABI Logic Selection" section, define a new INFIX_ABI_* macro based on the combination of OS and architecture.
c // In src/common/infix_config.h, inside the #ifndef INFIX_ABI_FORCED block... #if defined(INFIX_ARCH_AARCH64) // ... #elif defined(INFIX_ARCH_RISCV64) #define INFIX_ABI_LP64D // LP64D is the standard RISC-V ABI #elif defined(INFIX_ARCH_X64) // ... #endif
This is the core of the porting effort. You must provide a concrete implementation of the two ABI "specification" v-tables: infix_forward_abi_spec and infix_reverse_abi_spec.
src/arch/riscv64/, and add the necessary files. It's best to copy and adapt from an existing architecture like aarch64.abi_riscv64.cabi_riscv64_common.habi_riscv64_emitters.cabi_riscv64_emitters.h**Define Register Enums (abi_riscv64_common.h)**: Create enums for the general-purpose registers (GPRs) and floating-point registers (FPRs). The enum values should correspond to their 5-bit encoding in machine code.
```c // In src/arch/riscv64/abi_riscv64_common.h typedef enum { ZERO_REG = 0, RA_REG = 1, SP_REG = 2, /* ... */ A0_REG = 10, A1_REG = 11, /* ... */ } riscv_gpr;
typedef enum { FT0_REG = 0, /* ... */ FA0_REG = 10, FA1_REG = 11, /* ... */ } riscv_fpr; ```
**Implement ABI Logic (abi_riscv64.c)**: This file will contain the implementations for the ten functions required by the ABI specs. The prepare_forward_call_frame function is the most complex, as it must correctly apply all the ABI rules you researched in Step 0.
```c // In src/arch/riscv64/abi_riscv64.c #include "common/infix_internals.h" #include "abi_riscv64_common.h" #include "abi_riscv64_emitters.h"
// Forward Declarations for all 10 required static functions... static infix_status prepare_forward_call_frame_riscv64(...); static infix_status generate_forward_prologue_riscv64(...); // ... etc. for all 10 functions.
// Define the ABI Specification V-Table Instances const infix_forward_abi_spec g_riscv_forward_spec = { .prepare_forward_call_frame = prepare_forward_call_frame_riscv64, .generate_forward_prologue = generate_forward_prologue_riscv64, // ... fill in all 5 function pointers }; const infix_reverse_abi_spec g_riscv_reverse_spec = { .prepare_reverse_call_frame = prepare_reverse_call_frame_riscv64, // ... fill in all 5 function pointers };
// Implementation of all 10 functions... static infix_status prepare_forward_call_frame_riscv64(/*...*/) { // Core classification logic: // 1. Allocate the layout struct from the provided arena. // 2. Check for return-by-reference (consumes a0). // 3. Loop through arguments, classifying each one. // 4. Assign arguments to a0-a7, fa0-fa7, or the stack based on classification. // 5. Calculate total stack space needed and return the completed layout. } // ... implement the other 9 functions ... ```
In src/arch/riscv64/abi_riscv64_emitters.c and its header, you will write small, focused functions to generate the 32-bit RISC-V machine code instructions. Each function will assemble the bitfields specified in the ISA manual.
Example Emitter for LD (Load Doubleword):
The final step is to hook your new implementation into the main trampoline engine.
infix_internals.h: Add an include for your new emitters header inside the architecture-specific block at the bottom of the file. c // In src/common/infix_internals.h #if defined(INFIX_ABI_SYSV_X64) || defined(INFIX_ABI_WINDOWS_X64) #include "arch/x64/abi_x64_emitters.h" #elif defined(INFIX_ABI_AAPCS64) #include "arch/aarch64/abi_arm64_emitters.h" #elif defined(INFIX_ABI_LP64D) #include "arch/riscv64/abi_riscv64_emitters.h" // Add this line #endif Update trampoline.c:
extern declaration for your new spec v-tables inside a new #if defined(INFIX_ABI_LP64D) block.get_current_forward_abi_spec() and get_current_reverse_abi_spec() functions..c files to the unity build section at the very end of the file.```c // In src/core/trampoline.c
// ... near the top ... #elif defined(INFIX_ABI_AAPCS64) extern const infix_forward_abi_spec g_arm64_forward_spec; extern const infix_reverse_abi_spec g_arm64_reverse_spec; #elif defined(INFIX_ABI_LP64D) extern const infix_forward_abi_spec g_riscv_forward_spec; extern const infix_reverse_abi_spec g_riscv_reverse_spec; #endif
// ... in get_current_forward_abi_spec() ... #elif defined(INFIX_ABI_AAPCS64) return &g_arm64_forward_spec; #elif defined(INFIX_ABI_LP64D) return &g_riscv_forward_spec; #else
// ... (repeat for get_current_reverse_abi_spec) ...
// ... at the very bottom (unity build section) ... #elif defined(INFIX_ABI_AAPCS64) #include "../arch/aarch64/abi_arm64.c" #include "../arch/aarch64/abi_arm64_emitters.c" #elif defined(INFIX_ABI_LP64D) #include "../arch/riscv64/abi_riscv64.c" #include "../arch/riscv64/abi_riscv64_emitters.c" #else #error "No supported ABI was selected for the unity build in trampoline.c." #endif ```
Once the library compiles for your new target, the final and most important step is to run the entire test suite. This is best done on real hardware, but a high-fidelity emulator like QEMU can also be used.
101_by_value.c, 102_by_reference.c, and 402_variadic_functions.c, as these are the most likely to reveal subtle ABI implementation errors.