view release on metacpan or search on metacpan
CONTRIBUTING.md view on Meta::CPAN
First off, thank you for considering contributing! Affix and [infix](https://github.com/sanko/infix) are projects that value high-quality, well-researched technical contributions. We welcome submissions of all kinds, from documentation fixes to the i...
## Code of Conduct
This project and everyone participating in it is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.
## How to Contribute
We use a standard GitHub fork-and-pull-request workflow.
1. **Find or Create an Issue**: Before starting significant work, please check the [issue tracker](https://github.com/sanko/Affix.pm/issues) to see if there's an existing issue for what you want to do. If not, please create one to start a discussion...
2. **Fork the Repository**: Start by forking the main repository to your own GitHub account.
3. **Create a Branch**: Create a new branch for your feature or bugfix from the `main` branch. Please use a descriptive name.
```bash
# For a new feature:
git checkout -b feature/add-riscv-support
# For a bug fix:
git checkout -b fix/struct-classification-bug
```
Most of this version's work went into threading stability, ABI correctness, and security within the JIT engine.
### Changed
- [[infix]] The JIT memory allocator on Linux now uses `memfd_create` (on kernels 3.17+) to create anonymous file descriptors for dual-mapped W^X memory. This avoids creating visible temporary files in `/dev/shm` and improves hygiene and security. ...
- \[infix] On dual-mapped platforms (Linux/BSD), the Read-Write view of the JIT memory is now **unmapped immediately** after code generation. This closes a security window where an attacker with a heap read/write primitive could potentially modify ...
- \[infix] `infix_library_open` now uses `RTLD_LOCAL` instead of `RTLD_GLOBAL` on POSIX systems. This prevents symbols from loaded libraries from polluting the global namespace and causing conflicts with other plugins or the host application.
### Fixed
- Fixed `CLONE` to correctly copy user-defined types (typedefs, structs) to new threads. Previously, child threads started with an empty registry, causing lookup failures for types defined in the parent.
- Thread safety: Fixed a crash when callbacks are invoked from foreign threads. Affix now correctly injects the Perl interpreter context into the TLS before executing the callback.
- Added stack overflow protection to the FFI trigger. Argument marshalling buffers larger than 2KB are now allocated on the heap (arena) instead of the stack, preventing crashes on Windows and other platforms with limited stack sizes.
- Type resolution: Fixed a logic bug where `Pointer[SV]` types were incorrectly treated as generic pointers if `typedef`'d. They are now correctly unwrapped into Perl CODE refs or blessed objects.
- Process exit: Disabled explicit library unloading (`dlclose`/`FreeLibrary`) during global destruction. This prevents segmentation faults when background threads from loaded libraries try to execute code that has been unmapped from memory during s...
I tried to just limit it to Go lang libs but it's just more trouble than it's worth until I resolve a few more things.
- \[infix] Fixed stack corruption on macOS ARM64 (Apple Silicon). `long double` on this platform is 8 bytes (an alias for `double`), unlike standard AAPCS64 where it is 16 bytes. The JIT previously emitted 16-byte stores (`STR Qn`) for these types,...
- \[infix] Fixed `long double` handling on macOS Intel (Darwin). Verified that Apple adheres to the System V ABI for this type: it requires 16-byte stack alignment and returns values on the x87 FPU stack (`ST(0)`).
- \[infix] Fixed a generic System V ABI bug where 128-bit types (vectors, `__int128`) were not correctly aligned to 16 bytes on the stack relative to the return address, causing data corruption when mixed with odd numbers of 8-byte arguments.
- \[infix] Enforced natural alignment for stack arguments in the AAPCS64 implementation. Previously, arguments were packed to 8-byte boundaries, which violated alignment requirements for 128-bit types.
- \[infix] Fixed a critical deployment issue where the public `infix.h` header included an internal file (`common/compat_c23.h`). The header is now fully self-contained and defines `INFIX_NODISCARD` for attribute compatibility.
- 1. **Prevent Mangling:** Wrap your exported functions in `extern "C"` to ensure they have predictable names.
```
extern "C" {
int add(int a, int b) { return a + b; }
}
```
- 2. **Or Use Mangled Names:** If you cannot change the C++ source, you must look up the exact mangled name (e.g., `_Z3addii`) using tools like `nm` or `objdump`, and bind to that.
- 3. **Object Methods:** Calling an object's method requires passing the object instance pointer (the `this` pointer) as the first argument. Use the `ThisCall( ... )` wrapper around your callback/signature to automatically insert `Pointer[Void]` at t...
## Rust
Rust does not use the C ABI by default. You must explicitly instruct the compiler to format the function correctly.
- 1. **Exporting:** Use `#[no_mangle]` and `pub extern "C"`.
```rust
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }
### `set_destruct_level( $level )`
Sets the internal `PL_perl_destruct_level` variable.
When testing XS/FFI code for memory leaks using tools like Valgrind or AddressSanitizer, you often want Perl to
meticulously clean up all global memory during its destruction phase (otherwise the leak checker will be flooded with
false-positive "leaks" that are actually just memory Perl intentionally leaves to the OS to reclaim).
```
# Call this at the start of your script when running under Valgrind
set_destruct_level(2);
```
# COMPANION MODULES
Affix ships with two powerful companion modules to streamline your FFI development:
- [**Affix::Wrap**](https://metacpan.org/pod/Affix%3A%3AWrap): Parses C/C++ headers using the Clang AST to automatically generate Affix bindings for entire libraries.
- [**Affix::Build**](https://metacpan.org/pod/Affix%3A%3ABuild): A polyglot builder that compiles inline C, C++, Rust, Zig, Go, and 15+ other languages into dynamic libraries you can bind instantly.
# THREAD SAFETY & CONCURRENCY
Affix bridges Perl (a single-threaded interpreter, generally) with libraries that may be multi-threaded. This creates
potential hazards that you must manage.
## 1. Initialization Phase vs. Execution Phase
Functions that modify Affix's global state are **not thread-safe**. You must perform all definitions in the main thread
before starting any background threads or loops in the library.
Unsafe operations that you should never call from Callbacks or in a threaded context:
- `affix( ... )` - Binding new functions.
- `typedef( ... )` - Registering new types.
## 2. Callbacks
When passing a Perl subroutine as a `Callback`, avoid performing complex Perl operations like loading modules or
defining subs inside callbacks triggered on a foreign thread. Such callbacks should remain simple: process data, update
infix/include/infix/infix.h view on Meta::CPAN
} named_reference;
} meta;
};
/**
* @struct infix_struct_member_t
* @brief Describes a single member of a C struct or union.
*/
struct infix_struct_member_t {
const char * name; /**< The name of the member, or `nullptr` if anonymous. */
infix_type * type; /**< The `infix_type` of the member. */
size_t offset; /**< The byte offset of the member from the start of the aggregate. */
uint8_t bit_width; /**< The width of the bitfield in bits. 0 for standard members. */
uint8_t bit_offset; /**< The bit offset within the byte (0-7). */
bool is_bitfield; /**< True if this member is a bitfield (even if width is 0). */
};
/**
* @struct infix_function_argument_t
* @brief Describes a single argument to a C function.
*/
struct infix_function_argument_t {
const char * name; /**< The name of the argument, or `nullptr` if anonymous. */
infix/src/arch/aarch64/abi_arm64.c view on Meta::CPAN
*
* @section platform_deviations Platform-Specific Deviations
*
* - **Variadic Calls (Apple macOS):** All variadic arguments are passed on the
* stack. Arguments smaller than 8 bytes are promoted to fill 8-byte stack slots.
*
* - **Variadic Calls (Windows on ARM):** The HFA rule is disabled for variadic
* arguments. Floating-point scalars are passed in GPRs, not VPRs.
*
* - **16-Byte Argument Alignment:**
* - **Standard/macOS:** 16-byte aggregates passed in GPRs must start in an
* even-numbered register (X0, X2, X4, X6).
* - **macOS Exception:** `__int128_t` does NOT require even-GPR alignment.
* - **Windows Exception:** Variadic 16-byte aggregates do NOT require even-GPR alignment.
* @endinternal
*/
#include "arch/aarch64/abi_arm64_common.h"
#include "arch/aarch64/abi_arm64_emitters.h"
#include "common/infix_internals.h"
#include "common/utility.h"
#include <stdbool.h>
infix/src/arch/aarch64/abi_arm64.c view on Meta::CPAN
bool needs_alignment = true;
#if defined(INFIX_OS_MACOS)
// macOS Deviation: `__int128_t` does not require even-GPR alignment.
if (type->category == INFIX_TYPE_PRIMITIVE)
needs_alignment = false;
#elif defined(INFIX_OS_WINDOWS)
// Windows Deviation: Variadic 16-byte arguments do not require even-GPR alignment.
if (is_variadic_arg)
needs_alignment = false;
#endif
// Standard rule: 16-byte args must start in an even-numbered GPR.
if (needs_alignment && (gpr_count % 2 != 0))
gpr_count++;
if (gpr_count + 1 < NUM_GPR_ARGS) {
layout->arg_locations[i].type = ARG_LOCATION_GPR_PAIR;
layout->arg_locations[i].reg_index = (uint8_t)gpr_count;
gpr_count += 2;
placed_in_register = true;
}
}
else { // Types <= 8 bytes passed in a single GPR.
infix/src/arch/aarch64/abi_arm64.c view on Meta::CPAN
if (return_in_memory) {
// str x8, [sp, #return_buffer_offset]
emit_arm64_str_imm(buf, true, X8_REG, SP_REG, layout->return_buffer_offset);
}
// Iterate over arguments
size_t gpr_idx = 0;
size_t vpr_idx = 0;
size_t current_saved_data_offset = 0;
// Arguments passed on the caller's stack start at offset 16 from our new frame pointer (X29).
// X29 was established after pushing X29, X30, X19, X20, X21, X22.
// [X29] -> saved X29
// [X29+8] -> saved X30 (LR)
// [X29+16] -> caller's first stack argument
size_t caller_stack_offset = 16;
for (size_t i = 0; i < context->num_args; ++i) {
infix_type * type = context->arg_types[i];
bool is_variadic_arg = i >= context->num_fixed_args;
infix/src/arch/x64/abi_sysv_x64.c view on Meta::CPAN
/**
* @internal
* @brief Recursively classifies the eightbytes of an aggregate type.
* @details This is the core of the complex System V classification algorithm. It traverses
* the fields of a struct/array, examining each 8-byte chunk ("eightbyte") and assigning it a
* class (INTEGER, SSE, MEMORY). The classification is "merged" according to ABI rules
* (e.g., if an eightbyte contains both INTEGER and SSE parts, it becomes INTEGER).
*
* @param type The type of the current member/element being examined.
* @param offset The byte offset of this member from the start of the aggregate.
* @param[in,out] classes An array of two `arg_class_t` that is updated during classification.
* @param depth The current recursion depth (to prevent stack overflow on malicious input).
* @param field_count A counter to prevent DoS from excessively complex types.
* @param is_bitfield True if the current member is a bitfield.
* @return `true` if a condition forcing MEMORY classification is found, `false` otherwise.
*/
static bool classify_recursive(
const infix_type * type, size_t offset, arg_class_t classes[2], int depth, size_t * field_count, bool is_bitfield) {
// A recursive call can be made with a NULL type (e.g., from a malformed array from fuzzer).
if (type == nullptr)
infix/src/arch/x64/abi_sysv_x64.c view on Meta::CPAN
// If a struct is packed, its layout is explicit and should not be inferred
// by recursive classification. Treat it as an opaque block of memory.
// For classification purposes, this is equivalent to an integer array.
if (type->category == INFIX_TYPE_PRIMITIVE) {
(*field_count)++;
// `long double` is a special case. It is passed in memory on the stack, not x87 registers.
if (is_long_double(type)) {
classes[0] = MEMORY;
return true;
}
// Consider all eightbytes that the primitive occupies, not just the starting offset.
size_t start_offset = offset;
// Check for overflow before calculating end_offset
if (type->size == 0)
return false;
if (start_offset > SIZE_MAX - (type->size - 1)) {
classes[0] = MEMORY;
return true;
}
size_t end_offset = start_offset + type->size - 1;
size_t start_eightbyte = start_offset / 8;
size_t end_eightbyte = end_offset / 8;
arg_class_t new_class = (is_float16(type) || is_float(type) || is_double(type)) ? SSE : INTEGER;
for (size_t index = start_eightbyte; index <= end_eightbyte && index < 2; ++index) {
// Merge the new class with the existing class for this eightbyte.
// The rule is: if an eightbyte contains both SSE and INTEGER parts, it is classified as INTEGER.
if (classes[index] != new_class)
classes[index] = (classes[index] == NO_CLASS) ? new_class : INTEGER;
}
return false;
}
if (type->category == INFIX_TYPE_POINTER) {
(*field_count)++;
size_t index = offset / 8;
if (index < 2 && classes[index] != INTEGER)
classes[index] = INTEGER; // Pointers are always INTEGER class. Merge with existing class.
return false;
}
if (type->category == INFIX_TYPE_ARRAY) {
if (type->meta.array_info.element_type == nullptr)
return false;
// If the array elements have no size, iterating over them is pointless
// and can cause a timeout if num_elements is large, as the offset never advances.
// We only need to classify the element type once at the starting offset.
if (type->meta.array_info.element_type->size == 0) {
if (type->meta.array_info.num_elements > 0)
// Classify the zero-sized element just once.
return classify_recursive(
type->meta.array_info.element_type, offset, classes, depth + 1, field_count, false);
return false; // An empty array of zero-sized structs has no effect on classification.
}
for (size_t i = 0; i < type->meta.array_info.num_elements; ++i) {
// Check count *before* each recursive call inside the loop.
if (*field_count > MAX_AGGREGATE_FIELDS_TO_CLASSIFY) {
infix/src/arch/x64/abi_sysv_x64.c view on Meta::CPAN
return true;
}
infix_struct_member * member = &type->meta.aggregate_info.members[i];
// A generated type can have a NULL member type.
// This is invalid, and the aggregate must be passed in memory.
if (member->type == nullptr) {
classes[0] = MEMORY;
return true;
}
size_t member_offset = offset + member->offset;
// If this member starts at or after the 16-byte boundary,
// it cannot influence register classification, so we can skip it.
if (member_offset >= 16)
continue;
if (classify_recursive(member->type, member_offset, classes, depth + 1, field_count, member->is_bitfield))
return true; // Propagate unaligned discovery
}
return false;
}
return false;
}
infix/src/arch/x64/abi_sysv_x64.c view on Meta::CPAN
return INFIX_ERROR_LAYOUT_FAILED;
}
// The total allocation for the stack frame must be aligned to the maximum required alignment.
layout->total_stack_alloc = (uint32_t)_infix_align_up(total_local_space, max_align);
// Local variables are accessed via negative offsets from the frame pointer (RBP).
// The layout is [ return_buffer | args_array | (padding) | saved_args_data ]
layout->return_buffer_offset = -(int32_t)layout->total_stack_alloc;
layout->args_array_offset = layout->return_buffer_offset + (int32_t)return_size;
// Align the start of the saved data area
layout->saved_args_offset =
(int32_t)_infix_align_up((size_t)(layout->args_array_offset + args_array_size), max_align);
layout->max_align = (uint32_t)max_align;
*out_layout = layout;
return INFIX_SUCCESS;
}
/**
* @internal
* @brief Stage 2 (Reverse): Generates the prologue for the reverse trampoline stub.
infix/src/arch/x64/abi_sysv_x64.c view on Meta::CPAN
if (num_ret_classes > 0 && ret_classes[0] == MEMORY)
return_in_memory = true;
}
}
// The long double primitive is a special case that does not use the hidden pointer.
if (is_long_double(ret_type))
return_in_memory = false;
// If the return value is passed by reference, save the pointer from RDI.
if (return_in_memory)
emit_mov_mem_reg(buf, RBP_REG, layout->return_buffer_offset, GPR_ARGS[gpr_idx++]); // mov [rbp + offset], rdi
// Stack arguments passed by the caller start at [rbp + 16].
size_t stack_arg_offset = 16;
for (size_t i = 0; i < context->num_args; i++) {
infix_type * current_type = context->arg_types[i];
current_saved_data_offset = _infix_align_up(current_saved_data_offset, current_type->alignment);
int32_t arg_save_loc = layout->saved_args_offset + current_saved_data_offset;
// Correct classification logic for vectors/primitives vs aggregates
arg_class_t classes[2] = {NO_CLASS, NO_CLASS};
size_t num_classes = 0;
bool is_aggregate =
infix/src/common/double_tap.h view on Meta::CPAN
#define DBLTAP_PRINTF_FORMAT(fmt_index, arg_index)
#endif
// Public Test Harness Functions (wrapped by macros for convenience)
void tap_init(void);
void tap_plan(size_t count);
int tap_done(void);
void tap_bail_out(const char * reason, ...) DBLTAP_PRINTF_FORMAT(1, 2);
bool tap_ok(bool condition, const char * file, int line, const char * func, const char * expr, const char * name, ...)
DBLTAP_PRINTF_FORMAT(6, 7);
bool tap_subtest_start(const char * name);
bool tap_subtest_end(void);
void tap_todo_start(const char * reason, ...) DBLTAP_PRINTF_FORMAT(1, 2);
void tap_todo_end(void);
void tap_skip(size_t count, const char * reason, ...) DBLTAP_PRINTF_FORMAT(2, 3);
void tap_skip_all(const char * reason, ...) DBLTAP_PRINTF_FORMAT(1, 2);
void diag(const char * fmt, ...) DBLTAP_PRINTF_FORMAT(1, 2);
void tap_note(const char * fmt, ...) DBLTAP_PRINTF_FORMAT(1, 2);
void test_body(void);
#ifdef __cplusplus
}
#endif
infix/src/common/double_tap.h view on Meta::CPAN
#define bail_out(...) tap_bail_out(__VA_ARGS__)
/** @brief The core assertion macro. Checks a condition and prints an "ok" or "not ok" TAP line with diagnostics on
* failure. */
#define ok(cond, ...) tap_ok(!!(cond), __FILE__, __LINE__, __func__, #cond, __VA_ARGS__)
/** @brief A convenience macro that always passes. Equivalent to `ok(true, ...)`. */
#define pass(...) ok(true, __VA_ARGS__)
/** @brief A convenience macro that always fails. Equivalent to `ok(false, ...)`. */
#define fail(...) ok(false, __VA_ARGS__)
/** @brief Defines a block of tests as a nested subtest, which gets its own plan and pass/fail status. */
#define subtest(name) \
for (bool _tap_subtest_once = tap_subtest_start(name); _tap_subtest_once; _tap_subtest_once = tap_subtest_end())
/** @brief Marks a specified number of subsequent tests as skipped with a given reason. */
#define skip(count, ...) tap_skip(count, __VA_ARGS__)
/** @brief Marks all subsequent tests in the current scope as skipped. */
#define skip_all(...) tap_skip_all(__VA_ARGS__)
/** @brief Defines a block of tests that are expected to fail. A failure in a TODO block does not fail the overall
* suite. */
#define TODO(reason) \
for (int _tap_todo_once = (tap_todo_start(reason), 1); _tap_todo_once; _tap_todo_once = (tap_todo_end(), 0))
/** @brief Prints a diagnostic message to stderr, prefixed with '#'. Standard TAP practice for auxiliary information. */
#define diag(...) diag(__VA_ARGS__)
/** @brief Prints a diagnostic message (a note) to stdout, prefixed with '#'. */
#ifndef note
#define note(...) tap_note(__VA_ARGS__)
#endif
/** @brief Defines the main test function body where all tests are written. */
#ifdef __cplusplus
#define TEST extern "C" void test_body(void)
#else
infix/src/common/double_tap.h view on Meta::CPAN
bool tap_ok(bool condition, const char * file, int line, const char * func, const char * expr, const char * name, ...) {
_tap_ensure_initialized();
if (current_state->skipping) {
current_state->count++;
return true;
}
char name_buffer[256] = {0};
if (name && name[0] != '\0') {
va_list args;
va_start(args, name);
vsnprintf(name_buffer, sizeof(name_buffer), name, args);
va_end(args);
}
current_state->count++;
if (!condition) {
if (current_state->todo)
current_state->failed_todo++;
else {
current_state->failed++;
if (current_state == &state_stack[0]) // Only increment global fail count for top-level tests
infix/src/common/double_tap.h view on Meta::CPAN
fprintf(stdout, "# ...\n");
}
fflush(stdout);
return condition;
}
void tap_skip(size_t count, const char * reason, ...) {
_tap_ensure_initialized();
char buffer[256];
va_list args;
va_start(args, reason);
vsnprintf(buffer, sizeof(buffer), reason, args);
va_end(args);
for (size_t i = 0; i < count; ++i) {
current_state->count++;
print_indent(stdout);
printf("ok %llu # SKIP %s\n", (unsigned long long)current_state->count, buffer);
}
fflush(stdout);
}
void tap_skip_all(const char * reason, ...) {
_tap_ensure_initialized();
current_state->skipping = true;
va_list args;
va_start(args, reason);
vsnprintf(current_state->skip_reason, sizeof(current_state->skip_reason), reason, args);
va_end(args);
}
void tap_todo_start(const char * reason, ...) {
_tap_ensure_initialized();
current_state->todo = true;
va_list args;
va_start(args, reason);
vsnprintf(current_state->todo_reason, sizeof(current_state->todo_reason), reason, args);
va_end(args);
}
void tap_todo_end(void) {
_tap_ensure_initialized();
current_state->todo = false;
current_state->todo_reason[0] = '\0';
}
void diag(const char * fmt, ...) {
_tap_ensure_initialized();
char buffer[1024];
va_list args;
va_start(args, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
print_indent(stderr);
fprintf(stderr, "# %s\n", buffer);
fflush(stderr);
}
void tap_note(const char * fmt, ...) {
_tap_ensure_initialized();
char buffer[1024];
va_list args;
va_start(args, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
print_indent(stdout);
fprintf(stdout, "# %s\n", buffer);
fflush(stdout);
}
void tap_bail_out(const char * reason, ...) {
fprintf(stderr, "Bail out! ");
va_list args;
va_start(args, reason);
vfprintf(stderr, reason, args);
va_end(args);
fprintf(stderr, "\n");
fflush(stderr);
exit(1);
}
bool tap_subtest_start(const char * name) {
_tap_ensure_initialized();
print_indent(stdout);
fprintf(stdout, "# Subtest: %s\n", name);
fflush(stdout);
push_state();
snprintf(current_state->subtest_name, sizeof(current_state->subtest_name), "%s", name);
return true; // Enters the `for` loop body.
}
bool tap_subtest_end(void) {
infix/src/common/infix_internals.h view on Meta::CPAN
size_t num_buckets; /**< The number of buckets in the hash table. */
size_t num_items; /**< The total number of items in the registry. */
_infix_registry_entry_t ** buckets; /**< The array of hash table buckets (heap-allocated). */
};
/**
* @struct parser_state
* @brief Holds the complete state of the recursive descent parser during a single parse operation.
*/
typedef struct {
const char * p; /**< The current read position (cursor) in the signature string. */
const char * start; /**< The beginning of the signature string, used for calculating error positions. */
infix_arena_t * arena; /**< The temporary arena for allocating the raw, unresolved type graph. */
int depth; /**< The current recursion depth, to prevent stack overflows. */
} parser_state;
/**
* @struct code_buffer
* @brief A dynamic buffer for staged machine code generation.
*/
typedef struct {
uint8_t * code; /**< A pointer to the code buffer, allocated from the arena. */
infix/src/common/infix_internals.h view on Meta::CPAN
uint8_t num_xmm_args; /**< The number of XMMs used for arguments. */
#endif
infix_arg_location * arg_locations; /**< An array of location info for each argument. */
bool return_value_in_memory; /**< `true` if the return value uses a hidden pointer argument (struct return). */
bool is_variadic; /**< `true` if the function is variadic. */
size_t num_stack_args; /**< The number of arguments passed on the stack. */
size_t num_args; /**< The total number of arguments. */
void * target_fn; /**< The target function address. */
uint32_t max_align; /**< Maximum required alignment for any argument or the stack. */
uint32_t prologue_size; /**< Size of the generated prologue in bytes. */
uint32_t epilogue_offset; /**< Offset from the start of the JIT block to the epilogue. */
} infix_call_frame_layout;
/**
* @struct infix_reverse_call_frame_layout
* @brief A complete layout blueprint for a reverse call frame.
* @details This structure serves as a plan for the JIT-compiled reverse call stub.
* It contains the offsets for all data structures that the stub needs to create
* on its stack frame before calling the universal C dispatcher.
*/
typedef struct {
size_t total_stack_alloc; /**< Total bytes of local stack space needed. */
infix/src/common/infix_internals.h view on Meta::CPAN
* This structure serves as the plan for the JIT engine, detailing every register,
* stack slot, and marshaller/write-back call needed to execute a direct FFI call.
*/
typedef struct {
size_t total_stack_alloc; ///< Total bytes to allocate on the stack for arguments and ABI-required space.
size_t num_args; ///< The total number of arguments.
void * target_fn; ///< The target C function address.
bool return_value_in_memory; ///< `true` if the return value uses a hidden pointer argument.
infix_direct_arg_layout * args; ///< An array of layout info for each argument.
uint32_t prologue_size; ///< Size of the generated prologue in bytes.
uint32_t epilogue_offset; ///< Offset from the start of the JIT block to the epilogue.
} infix_direct_call_frame_layout;
/**
* @brief Defines the ABI-specific implementation interface for direct marshalling forward trampolines.
*
* This v-table defines the contract for generating a high-performance, direct-marshalling
* trampoline. It is parallel to `infix_forward_abi_spec`.
*/
typedef struct {
/** @brief Analyzes a function signature to create a complete direct call frame layout. */
infix/src/common/utility.h view on Meta::CPAN
} while (0)
/**
* @internal
* @brief Declares the function prototype for `infix_dump_hex` for use in debug builds.
* @details This function is an invaluable tool for inspecting the raw machine code generated
* by the JIT compiler or examining the memory layout of complex structs. It prints
* a detailed hexadecimal and ASCII dump of a memory region to the standard
* output, formatted for readability.
*
* @param data A pointer to the start of the memory block to dump.
* @param size The number of bytes to dump.
* @param title A descriptive title to print before and after the hex dump.
*/
void infix_dump_hex(const void * data, size_t size, const char * title);
/**
* @internal
* @brief Declares the function prototype for `infix_dump_state` for use in debug builds.
* @details This function is an invaluable tool for inspecting the processor state.
*
* @param file The file name where the function is being called.
infix/src/core/error.c view on Meta::CPAN
g_infix_last_error.category = category;
g_infix_last_error.code = code;
g_infix_last_error.position = position;
g_infix_last_error.system_error_code = 0;
// Check if we can generate a rich parser error message.
if (category == INFIX_CATEGORY_PARSER && g_infix_last_signature_context != nullptr) {
// Generate a rich, GCC-style error message for parser failures.
const char * signature = g_infix_last_signature_context;
size_t sig_len = strlen(signature);
const size_t radius = 20; // Number of characters to show around the error position.
// Calculate the start and end of the snippet to display.
size_t start = (position > radius) ? (position - radius) : 0;
size_t end = (position + radius < sig_len) ? (position + radius) : sig_len;
// Add indicators if the snippet is truncated.
const char * start_indicator = (start > 0) ? "... " : "";
const char * end_indicator = (end < sig_len) ? " ..." : "";
size_t start_indicator_len = (start > 0) ? 4 : 0;
// Create the code snippet line.
char snippet[128];
snprintf(snippet,
sizeof(snippet),
"%s%.*s%s",
start_indicator,
(int)(end - start),
signature + start,
end_indicator);
// Create the pointer line with a caret '^' under the error.
char pointer[128];
size_t caret_pos = position - start + start_indicator_len;
snprintf(pointer, sizeof(pointer), "%*s^", (int)caret_pos, "");
// Build the final multi-line message piece by piece to avoid buffer overflows.
char * p = g_infix_last_error.message;
size_t remaining = sizeof(g_infix_last_error.message);
int written;
// Write the snippet and pointer lines.
written = snprintf(p, remaining, "\n\n %s\n %s", snippet, pointer);
if (written < 0 || (size_t)written >= remaining) {
// Fallback to a simple message on snprintf failure or buffer overflow.
const char * msg = _get_error_message_for_code(code);
infix/src/core/signature.c view on Meta::CPAN
size_t * out_num_args,
size_t * out_num_fixed_args);
// Parser Helper Functions
/**
* @internal
* @brief Sets a detailed parser error, capturing the current position in the string.
* @param[in,out] state The current parser state.
* @param[in] code The error code to set.
*/
INFIX_INTERNAL void _infix_set_parser_error(parser_state * state, infix_error_code_t code) {
_infix_set_error(INFIX_CATEGORY_PARSER, code, (size_t)(state->p - state->start));
}
INFIX_INTERNAL void skip_whitespace(parser_state * state);
/**
* @internal
* @brief Advances the parser's cursor past any whitespace or C-style line comments.
* @param[in,out] state The parser state to modify.
*/
INFIX_INTERNAL void skip_whitespace(parser_state * state) {
while (true) {
infix/src/core/signature.c view on Meta::CPAN
}
}
/**
* @internal
* @brief Parses an unsigned integer from the string, used for array/vector sizes.
* @param[in,out] state The parser state.
* @param[out] out_val A pointer to store the parsed value.
* @return `true` on success, `false` on failure.
*/
static bool parse_size_t(parser_state * state, size_t * out_val) {
const char * start = state->p;
char * end;
errno = 0; // Reset errno before call
unsigned long long val = strtoull(start, &end, 10);
// Check for no conversion (end==start) OR overflow (ERANGE)
if (end == start || errno == ERANGE) {
// Use INTEGER_OVERFLOW code for range errors
_infix_set_parser_error(state, errno == ERANGE ? INFIX_CODE_INTEGER_OVERFLOW : INFIX_CODE_UNEXPECTED_TOKEN);
return false;
}
// Check for truncation if size_t is smaller than unsigned long long (e.g. 32-bit builds)
if (val > SIZE_MAX) {
_infix_set_parser_error(state, INFIX_CODE_INTEGER_OVERFLOW);
return false;
}
infix/src/core/signature.c view on Meta::CPAN
/**
* @internal
* @brief Parses a C-style identifier from the string.
* @details This is used for member names, named types, and function argument names.
* It handles simple identifiers (`my_var`) and C++-style namespaces (`NS::Name`).
* @param[in,out] state The parser state.
* @return An arena-allocated string for the identifier, or `nullptr` on failure.
*/
static const char * parse_identifier(parser_state * state) {
skip_whitespace(state);
const char * start = state->p;
if (!isalpha((unsigned char)*start) && *start != '_')
return nullptr;
while (isalnum((unsigned char)*state->p) || *state->p == '_' || *state->p == ':') {
if (*state->p == ':' && state->p[1] != ':')
break; // A single ':' is not part of an identifier.
if (*state->p == ':')
state->p++; // Consume first ':' of '::'
state->p++;
}
size_t len = state->p - start;
if (len == 0)
return nullptr;
char * name = infix_arena_calloc(state->arena, 1, len + 1, 1);
if (!name) {
_infix_set_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, (size_t)(state->p - state->start));
return nullptr;
}
infix_memcpy((void *)name, start, len);
name[len] = '\0';
return name;
}
/**
* @internal
* @brief Consumes a specific keyword from the string (e.g., "int", "struct").
* @details This function is careful to match whole words only. For example, it will
* successfully consume "int" from "int x", but will fail on "integer", preventing
* false positives.
* @param[in,out] state The parser state.
infix/src/core/signature.c view on Meta::CPAN
return nullptr;
}
bit_width = (uint8_t)width_val;
is_bitfield = true;
}
}
member_node * node = infix_arena_calloc(state->arena, 1, sizeof(member_node), _Alignof(member_node));
if (!node) {
_infix_set_error(
INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, (size_t)(state->p - state->start));
return nullptr;
}
// The member offset is not calculated here; it will be done later
// by `infix_type_create_struct` or `_infix_type_recalculate_layout`.
if (is_bitfield)
node->m = infix_type_create_bitfield_member(name, member_type, 0, bit_width);
else
node->m = infix_type_create_member(name, member_type, 0);
node->next = nullptr;
infix/src/core/signature.c view on Meta::CPAN
}
}
}
*out_num_members = num_members;
if (num_members == 0)
return nullptr;
// Convert the temporary linked list to a flat array in the arena.
infix_struct_member * members =
infix_arena_calloc(state->arena, num_members, sizeof(infix_struct_member), _Alignof(infix_struct_member));
if (!members) {
_infix_set_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, (size_t)(state->p - state->start));
return nullptr;
}
member_node * current = head;
for (size_t i = 0; i < num_members; i++) {
members[i] = current->m;
current = current->next;
}
return members;
}
/**
* @internal
* @brief Parses a struct (`{...}`) or union (`<...>`).
* @param[in,out] state The parser state.
* @param[in] start_char The opening delimiter ('{' or '<').
* @param[in] end_char The closing delimiter ('}' or '>').
* @return A pointer to the new `infix_type`, or `nullptr` on failure.
*/
static infix_type * parse_aggregate(parser_state * state, char start_char, char end_char) {
if (state->depth >= MAX_RECURSION_DEPTH) {
_infix_set_parser_error(state, INFIX_CODE_RECURSION_DEPTH_EXCEEDED);
return nullptr;
}
state->depth++;
if (*state->p != start_char) {
_infix_set_parser_error(state, INFIX_CODE_UNEXPECTED_TOKEN);
state->depth--;
return nullptr;
}
state->p++;
size_t num_members = 0;
infix_struct_member * members = parse_aggregate_members(state, end_char, &num_members);
// If member parsing failed, an error is already set. Propagate the failure.
if (!members && infix_get_last_error().code != INFIX_CODE_SUCCESS) {
state->depth--;
return nullptr;
}
if (*state->p != end_char) {
_infix_set_parser_error(state, INFIX_CODE_UNTERMINATED_AGGREGATE);
state->depth--;
return nullptr;
}
state->p++;
infix_type * agg_type = nullptr;
infix_status status = (start_char == '{') ? infix_type_create_struct(state->arena, &agg_type, members, num_members)
: infix_type_create_union(state->arena, &agg_type, members, num_members);
if (status != INFIX_SUCCESS) {
state->depth--;
return nullptr;
}
state->depth--;
return agg_type;
}
/**
* @internal
infix/src/core/signature.c view on Meta::CPAN
* @param[in,out] state The parser state.
* @return A pointer to the parsed `infix_type`, or `nullptr` on failure.
*/
INFIX_INTERNAL infix_type * parse_type(parser_state * state) {
if (state->depth >= MAX_RECURSION_DEPTH) {
_infix_set_parser_error(state, INFIX_CODE_RECURSION_DEPTH_EXCEEDED);
return nullptr;
}
state->depth++;
skip_whitespace(state);
// Capture the offset from the start of the signature string *before* parsing the type.
size_t current_offset = state->p - state->start;
infix_type * result_type = nullptr;
const char * p_before_type = state->p;
if (*state->p == '@') { // Named type reference: `@MyStruct`
state->p++;
const char * name = parse_identifier(state);
if (!name) {
_infix_set_parser_error(state, INFIX_CODE_UNEXPECTED_TOKEN);
state->depth--;
return nullptr;
}
infix/src/core/signature.c view on Meta::CPAN
size_t num_args = 0, num_fixed = 0;
if (parse_function_signature_details(state, &ret_type, &args, &num_args, &num_fixed) != INFIX_SUCCESS) {
state->depth--;
return nullptr;
}
// Manually construct a function pointer type object.
// This is represented internally as a pointer-like type with extra metadata.
infix_type * func_type = infix_arena_calloc(state->arena, 1, sizeof(infix_type), _Alignof(infix_type));
if (!func_type) {
_infix_set_error(
INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, (size_t)(state->p - state->start));
state->depth--;
return nullptr;
}
func_type->size = sizeof(void *);
func_type->alignment = _Alignof(void *);
func_type->is_arena_allocated = true;
func_type->category = INFIX_TYPE_REVERSE_TRAMPOLINE; // Special category for function types.
func_type->meta.func_ptr_info.return_type = ret_type;
func_type->meta.func_ptr_info.args = args;
func_type->meta.func_ptr_info.num_args = num_args;
infix/src/core/signature.c view on Meta::CPAN
skip_whitespace(state);
if (*state->p == ')' || *state->p == ';')
break;
const char * name = parse_optional_name_prefix(state);
infix_type * arg_type = parse_type(state);
if (!arg_type)
return INFIX_ERROR_INVALID_ARGUMENT;
arg_node * node = infix_arena_calloc(state->arena, 1, sizeof(arg_node), _Alignof(arg_node));
if (!node) {
_infix_set_error(
INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, (size_t)(state->p - state->start));
return INFIX_ERROR_ALLOCATION_FAILED;
}
node->arg.type = arg_type;
node->arg.name = name;
node->next = nullptr;
if (!head)
head = tail = node;
else {
tail->next = node;
tail = node;
infix/src/core/signature.c view on Meta::CPAN
skip_whitespace(state);
if (*state->p == ')')
break;
const char * name = parse_optional_name_prefix(state);
infix_type * arg_type = parse_type(state);
if (!arg_type)
return INFIX_ERROR_INVALID_ARGUMENT;
arg_node * node = infix_arena_calloc(state->arena, 1, sizeof(arg_node), _Alignof(arg_node));
if (!node) {
_infix_set_error(
INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, (size_t)(state->p - state->start));
return INFIX_ERROR_ALLOCATION_FAILED;
}
node->arg.type = arg_type;
node->arg.name = name;
node->next = nullptr;
if (!head)
head = tail = node;
else {
tail->next = node;
tail = node;
infix/src/core/signature.c view on Meta::CPAN
}
state->p += 2;
*out_ret_type = parse_type(state);
if (!*out_ret_type)
return INFIX_ERROR_INVALID_ARGUMENT;
// Convert linked list of args to a flat array.
infix_function_argument * args = (num_args > 0)
? infix_arena_calloc(state->arena, num_args, sizeof(infix_function_argument), _Alignof(infix_function_argument))
: nullptr;
if (num_args > 0 && !args) {
_infix_set_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, (size_t)(state->p - state->start));
return INFIX_ERROR_ALLOCATION_FAILED;
}
arg_node * current = head;
for (size_t i = 0; i < num_args; i++) {
args[i] = current->arg;
current = current->next;
}
*out_args = args;
*out_num_args = num_args;
return INFIX_SUCCESS;
infix/src/core/signature.c view on Meta::CPAN
if (!signature || *signature == '\0') {
_infix_set_error(INFIX_CATEGORY_PARSER, INFIX_CODE_EMPTY_SIGNATURE, 0);
return INFIX_ERROR_INVALID_ARGUMENT;
}
// The top-level public API is responsible for setting g_infix_last_signature_context.
*out_arena = infix_arena_create(4096);
if (!*out_arena) {
_infix_set_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, 0);
return INFIX_ERROR_ALLOCATION_FAILED;
}
parser_state state = {.p = signature, .start = signature, .arena = *out_arena, .depth = 0};
infix_type * type = parse_type(&state);
if (type) {
skip_whitespace(&state);
// After successfully parsing a type, ensure there is no trailing junk.
if (state.p[0] != '\0') {
_infix_set_parser_error(&state, INFIX_CODE_UNEXPECTED_TOKEN);
type = nullptr;
}
}
if (!type) {
infix/src/core/signature.c view on Meta::CPAN
* @brief A safe `vsnprintf` wrapper for building the signature string.
* Updates the printer state and sets an error on buffer overflow.
* @param[in,out] state The printer state.
* @param[in] fmt The `printf`-style format string.
* @param[in] ... Arguments for the format string.
*/
static void _print(printer_state * state, const char * fmt, ...) {
if (state->status != INFIX_SUCCESS)
return;
va_list args;
va_start(args, fmt);
int written = vsnprintf(state->p, state->remaining, fmt, args);
va_end(args);
if (written < 0 || (size_t)written >= state->remaining)
// If snprintf failed or would have overflowed, mark an error.
state->status = INFIX_ERROR_INVALID_ARGUMENT;
else {
state->p += written;
state->remaining -= written;
}
}
infix/src/core/type_registry.c view on Meta::CPAN
/**
* @internal
* @struct _registry_parser_state_t
* @brief A minimal parser state for processing registry definition strings.
* @details This is distinct from the main signature `parser_state` because the
* grammar for definitions (`@Name=...;`) is much simpler and can be handled
* with a simpler, non-recursive parser.
*/
typedef struct {
const char * p; /**< The current position in the definition string. */
const char * start; /**< The start of the definition string for error reporting. */
} _registry_parser_state_t;
/**
* @internal
* @brief Skips whitespace and C++-style line comments in a definition string.
* @param state The parser state to advance.
*/
static void _registry_parser_skip_whitespace(_registry_parser_state_t * state) {
while (1) {
while (isspace((unsigned char)*state->p))
state->p++;
infix/src/core/type_registry.c view on Meta::CPAN
/**
* @internal
* @brief Parses a type name (e.g., `MyType`, `NS::MyType`) from the definition string.
* @param state The parser state to advance.
* @param buffer A buffer to store the parsed name.
* @param buf_size The size of the buffer.
* @return A pointer to the `buffer` on success, or `nullptr` if no valid identifier is found.
*/
static char * _registry_parser_parse_name(_registry_parser_state_t * state, char * buffer, size_t buf_size) {
_registry_parser_skip_whitespace(state);
const char * name_start = state->p;
if (!isalpha((unsigned char)*name_start) && *name_start != '_')
return nullptr;
while (isalnum((unsigned char)*state->p) || *state->p == '_' || *state->p == ':') {
if (*state->p == ':' && state->p[1] != ':')
break; // Handle single colon as non-identifier char.
if (*state->p == ':')
state->p++; // Skip the first ':' of '::'
state->p++;
}
size_t len = state->p - name_start;
if (len == 0 || len >= buf_size)
return nullptr;
infix_memcpy(buffer, name_start, len);
buffer[len] = '\0';
return buffer;
}
/**
* @brief Parses a string of type definitions and adds them to a registry.
*
* @details This function uses a robust **three-pass approach** to handle complex dependencies,
* including out-of-order and mutually recursive definitions.
*
* - **Pass 1 (Scan & Index):** The entire definition string is scanned. The parser
infix/src/core/type_registry.c view on Meta::CPAN
* @param[in] registry The registry to populate.
* @param[in] definitions A semicolon-separated string of definitions.
* @return `INFIX_SUCCESS` on success, or an error code on failure.
*/
INFIX_API c23_nodiscard infix_status infix_register_types(infix_registry_t * registry, const char * definitions) {
_infix_clear_error();
if (!registry || !definitions) {
_infix_set_error(INFIX_CATEGORY_GENERAL, INFIX_CODE_NULL_POINTER, 0);
return INFIX_ERROR_INVALID_ARGUMENT;
}
_registry_parser_state_t state = {.p = definitions, .start = definitions};
g_infix_last_signature_context = definitions;
// A temporary structure to hold information about each definition found in Pass 1.
struct def_info {
_infix_registry_entry_t * entry;
const char * def_body_start;
size_t def_body_len;
};
size_t defs_capacity = 64; // Start with an initial capacity.
struct def_info * defs_found = infix_malloc(sizeof(struct def_info) * defs_capacity);
if (!defs_found) {
_infix_set_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, 0);
return INFIX_ERROR_ALLOCATION_FAILED;
}
size_t num_defs_found = 0;
infix_status final_status = INFIX_SUCCESS;
// Pass 1: Scan & Index all names and their definition bodies.
while (*state.p != '\0') {
_registry_parser_skip_whitespace(&state);
if (*state.p == '\0')
break;
if (*state.p != '@') {
_infix_set_error(INFIX_CATEGORY_PARSER, INFIX_CODE_UNEXPECTED_TOKEN, state.p - state.start);
final_status = INFIX_ERROR_INVALID_ARGUMENT;
goto cleanup;
}
state.p++;
char name_buffer[256];
if (!_registry_parser_parse_name(&state, name_buffer, sizeof(name_buffer))) {
_infix_set_error(INFIX_CATEGORY_PARSER, INFIX_CODE_UNEXPECTED_TOKEN, state.p - state.start);
final_status = INFIX_ERROR_INVALID_ARGUMENT;
goto cleanup;
}
_infix_registry_entry_t * entry = _registry_lookup(registry, name_buffer);
_registry_parser_skip_whitespace(&state);
if (*state.p == '=') { // This is a full definition.
state.p++;
_registry_parser_skip_whitespace(&state);
// It's an error to redefine a type that wasn't a forward declaration.
if (entry && !entry->is_forward_declaration) {
_infix_set_error(INFIX_CATEGORY_PARSER, INFIX_CODE_UNEXPECTED_TOKEN, state.p - state.start);
final_status = INFIX_ERROR_INVALID_ARGUMENT;
goto cleanup;
}
if (!entry) { // If it doesn't exist, create it.
entry = _registry_insert(registry, name_buffer);
if (!entry) {
final_status = INFIX_ERROR_ALLOCATION_FAILED;
goto cleanup;
}
}
if (num_defs_found >= defs_capacity) {
size_t new_capacity = defs_capacity * 2;
struct def_info * new_defs = infix_realloc(defs_found, sizeof(struct def_info) * new_capacity);
if (!new_defs) {
_infix_set_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, state.p - state.start);
final_status = INFIX_ERROR_ALLOCATION_FAILED;
goto cleanup;
}
defs_found = new_defs;
defs_capacity = new_capacity;
}
// Find the end of the type definition body (the next top-level ';').
defs_found[num_defs_found].entry = entry;
defs_found[num_defs_found].def_body_start = state.p;
int nest_level = 0;
const char * body_end = state.p;
while (*body_end != '\0' &&
!(*body_end == ';' &&
nest_level == 0)) { // Explicitly check for and skip over the '->' token as a single unit.
if (*body_end == '-' && body_end[1] == '>') {
body_end += 2; // Advance the pointer past the entire token.
continue; // Continue to the next character in the loop.
}
if (*body_end == '{' || *body_end == '<' || *body_end == '(' || *body_end == '[')
infix/src/core/type_registry.c view on Meta::CPAN
body_end++;
}
defs_found[num_defs_found].def_body_len = body_end - state.p;
state.p = body_end;
num_defs_found++;
}
else if (*state.p == ';') { // This is a forward declaration.
if (entry) {
// If the type is already fully defined, re-declaring it as a forward decl is an error.
if (!entry->is_forward_declaration) {
_infix_set_error(INFIX_CATEGORY_PARSER, INFIX_CODE_UNEXPECTED_TOKEN, state.p - state.start);
final_status = INFIX_ERROR_INVALID_ARGUMENT;
goto cleanup;
}
// If it's already a forward declaration, this is a no-op.
}
else {
entry = _registry_insert(registry, name_buffer);
if (!entry) {
final_status = INFIX_ERROR_ALLOCATION_FAILED;
goto cleanup;
infix/src/core/type_registry.c view on Meta::CPAN
entry->type->is_incomplete = true; // Mark as incomplete
entry->type->arena = registry->arena;
entry->type->category = INFIX_TYPE_STRUCT;
entry->type->size = 0;
entry->type->alignment = 1; // Minimal alignment to pass basic validation
entry->type->name = entry->name;
}
entry->is_forward_declaration = true;
}
else {
_infix_set_error(INFIX_CATEGORY_PARSER, INFIX_CODE_UNEXPECTED_TOKEN, state.p - state.start);
final_status = INFIX_ERROR_INVALID_ARGUMENT;
goto cleanup;
}
if (*state.p == ';')
state.p++;
}
// Use a single temporary arena for ALL definitions in this batch to reduce malloc pressure.
infix_arena_t * parser_arena = infix_arena_create(65536);
if (!parser_arena) {
infix/src/core/type_registry.c view on Meta::CPAN
// Pass 2: Parse the bodies of all found definitions into the registry.
for (size_t i = 0; i < num_defs_found; ++i) {
_infix_registry_entry_t * entry = defs_found[i].entry;
// FAST-PATH: Check if the definition body is just a single primitive keyword.
// This avoids the overhead of the recursive-descent parser for simple aliases.
infix_type * fast_primitive = nullptr;
char body_buffer[64];
if (defs_found[i].def_body_len > 0 && defs_found[i].def_body_len < sizeof(body_buffer)) {
infix_memcpy(body_buffer, defs_found[i].def_body_start, defs_found[i].def_body_len);
body_buffer[defs_found[i].def_body_len] = '\0';
// Use a temporary parser state for parse_primitive.
parser_state fast_state = {.p = body_buffer, .start = body_buffer, .arena = parser_arena, .depth = 0};
fast_primitive = parse_primitive(&fast_state);
// Ensure no trailing junk even in fast-path.
if (fast_primitive) {
skip_whitespace(&fast_state);
if (*fast_state.p != '\0')
fast_primitive = nullptr;
}
}
infix/src/core/type_registry.c view on Meta::CPAN
else {
// SLOW-PATH: Fall back to full signature parser.
// Make a temporary, null-terminated copy of the definition body substring.
char * body_copy = infix_malloc(defs_found[i].def_body_len + 1);
if (!body_copy) {
_infix_set_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, 0);
final_status = INFIX_ERROR_ALLOCATION_FAILED;
infix_arena_destroy(parser_arena);
goto cleanup;
}
infix_memcpy(body_copy, defs_found[i].def_body_start, defs_found[i].def_body_len);
body_copy[defs_found[i].def_body_len] = '\0';
parser_state state = {.p = body_copy, .start = body_copy, .arena = parser_arena, .depth = 0};
raw_type = parse_type(&state);
if (raw_type) {
skip_whitespace(&state);
if (*state.p != '\0') {
_infix_set_parser_error(&state, INFIX_CODE_UNEXPECTED_TOKEN);
raw_type = nullptr;
}
}
if (!raw_type) {
infix_error_details_t err = infix_get_last_error();
_infix_set_error(err.category, err.code, (defs_found[i].def_body_start - definitions) + err.position);
final_status = INFIX_ERROR_INVALID_ARGUMENT;
infix_free(body_copy);
infix_arena_destroy(parser_arena);
goto cleanup;
}
infix_free(body_copy);
}
const infix_type * type_to_alias = raw_type;
// If the RHS is another named type (e.g., @MyAlias = @ExistingType),
// we need to resolve it first to get the actual type we're aliasing.
if (raw_type->category == INFIX_TYPE_NAMED_REFERENCE) {
_infix_registry_entry_t * existing_entry = _registry_lookup(registry, raw_type->meta.named_reference.name);
if (existing_entry && existing_entry->type)
type_to_alias = existing_entry->type;
else {
size_t relative_pos = raw_type->source_offset;
_infix_set_error(INFIX_CATEGORY_PARSER,
INFIX_CODE_UNRESOLVED_NAMED_TYPE,
(size_t)(defs_found[i].def_body_start - definitions) + relative_pos);
final_status = INFIX_ERROR_INVALID_ARGUMENT;
infix_arena_destroy(parser_arena);
goto cleanup;
}
}
// Prepare the new definition.
infix_type * new_def = nullptr;
if (!type_to_alias->is_arena_allocated) {
// This is a static type (e.g., primitive). We MUST create a mutable
// copy in the registry's arena before we can attach a name to it.
infix/src/core/type_registry.c view on Meta::CPAN
// Pass 3: Resolve and layout all the newly defined types.
for (size_t i = 0; i < num_defs_found; ++i) {
_infix_registry_entry_t * entry = defs_found[i].entry;
if (entry->type) {
// "Resolve" and "Layout" steps.
if (_infix_resolve_type_graph_inplace(&entry->type, registry) != INFIX_SUCCESS) {
// The error was set inside resolve (relative to body).
// We need to re-base it to the full definitions string.
infix_error_details_t err = infix_get_last_error();
size_t body_offset = defs_found[i].def_body_start - definitions;
_infix_set_error(err.category, err.code, body_offset + err.position);
final_status = INFIX_ERROR_INVALID_ARGUMENT;
goto cleanup;
}
_infix_type_recalculate_layout(entry->type);
}
}
cleanup:
infix_free(defs_found);
return final_status;
infix/src/core/type_registry.c view on Meta::CPAN
* @return `true` if the iterator was advanced to a valid type, or `false` if there are
* no more types to visit.
*/
INFIX_API c23_nodiscard bool infix_registry_iterator_next(infix_registry_iterator_t * iter) {
if (!iter || !iter->registry)
return false;
// Cast the opaque void* back to the internal entry type
const _infix_registry_entry_t * entry = (const _infix_registry_entry_t *)iter->_current_entry;
// If we have a current entry, start from the next one in the chain.
if (entry)
entry = entry->next;
// Otherwise, if we are starting (or moved buckets), begin with the head of the current bucket.
else if (iter->_bucket_index < iter->registry->num_buckets)
entry = iter->registry->buckets[iter->_bucket_index];
while (true) {
// Traverse the current chain looking for a valid entry.
while (entry) {
if (entry->type && !entry->is_forward_declaration) {
// Found one. Update the iterator and return successfully.
iter->_current_entry = (void *)entry;
return true;
infix/src/core/types.c view on Meta::CPAN
INFIX_API c23_nodiscard infix_type * infix_type_create_pointer(void) { return &_infix_type_pointer; }
/**
* @brief Creates a static descriptor for the `void` type.
* @return A pointer to the static `infix_type` descriptor. Does not need to be freed.
*/
INFIX_API c23_nodiscard infix_type * infix_type_create_void(void) { return &_infix_type_void; }
/**
* @brief A factory function to create an `infix_struct_member`.
* @param[in] name The name of the member (optional, can be `nullptr`).
* @param[in] type The `infix_type` of the member.
* @param[in] offset The byte offset of the member from the start of its parent aggregate.
* @return An initialized `infix_struct_member` object.
*/
INFIX_API infix_struct_member infix_type_create_member(const char * name, infix_type * type, size_t offset) {
return (infix_struct_member){name, type, offset, 0, 0, false};
}
/**
* @brief A factory function to create a bitfield `infix_struct_member`.
* @param[in] name The name of the member.
* @param[in] type The integer `infix_type` of the bitfield.
* @param[in] offset The byte offset (usually 0 for automatic layout).
infix/src/core/types.c view on Meta::CPAN
infix_struct_member * member = &type->meta.aggregate_info.members[i];
infix_type * mtype = member->type;
if (member->is_bitfield) {
size_t align = mtype->alignment;
if (align == 0)
align = 1;
if (align > max_alignment && !type->meta.aggregate_info.is_packed)
max_alignment = align;
// Zero-width bitfield or type mismatch or doesn't fit -> start new unit
if (member->bit_width == 0 || !in_bitfield || mtype->size != current_unit_size ||
current_unit_bits_used + member->bit_width > mtype->size * 8) {
// Align to start of new unit
size_t start = _infix_align_up(current_byte_offset, align);
if (start < current_byte_offset) {
_infix_set_error(INFIX_CATEGORY_PARSER, INFIX_CODE_INTEGER_OVERFLOW, 0);
return false;
}
current_unit_offset = start;
current_unit_size = mtype->size;
current_unit_bits_used = 0;
in_bitfield = true;
current_byte_offset = current_unit_offset;
}
member->offset = current_unit_offset + (current_unit_bits_used / 8);
member->bit_offset = (uint8_t)(current_unit_bits_used % 8);
current_unit_bits_used += member->bit_width;
infix/src/core/types.c view on Meta::CPAN
type->alignment = type->meta.array_info.element_type->alignment;
type->size = type->meta.array_info.element_type->size * type->meta.array_info.num_elements;
}
}
}
/**
* @internal
* @brief Public-internal wrapper for the recursive layout recalculation function.
*
* @details This function serves as the entry point for the "Layout" stage. It initializes
* the cycle detection mechanism and starts the recursive traversal of the type graph.
*
* @param[in,out] type The root of the type graph to recalculate. The graph is modified in-place.
*/
void _infix_type_recalculate_layout(infix_type * type) {
// Create a temporary arena solely for the visited list's lifetime.
infix_arena_t * temp_arena = infix_arena_create(1024);
if (!temp_arena)
return;
recalc_visited_node_t * visited_head = nullptr;
_infix_type_recalculate_layout_recursive(temp_arena, type, &visited_head);
infix/src/jit/executor.c view on Meta::CPAN
#define kCFStringEncodingUTF8 0x08000100
// A struct to hold dynamically loaded function pointers from macOS frameworks.
static struct {
void (*CFRelease)(CFTypeRef);
bool (*CFBooleanGetValue)(CFTypeRef boolean);
CFStringRef (*CFStringCreateWithCString)(CFTypeRef allocator, const char * cStr, uint32_t encoding);
CFTypeRef kCFAllocatorDefault;
SecTaskRef (*SecTaskCreateFromSelf)(CFTypeRef allocator);
CFTypeRef (*SecTaskCopyValueForEntitlement)(SecTaskRef task, CFStringRef entitlement, CFErrorRef * error);
void (*pthread_jit_write_protect_np)(int enabled);
void (*sys_icache_invalidate)(void * start, size_t len);
} g_macos_apis;
/**
* @internal
* @brief One-time initialization to dynamically load macOS framework functions.
* @details Uses `dlopen` and `dlsym` to find the necessary CoreFoundation and Security
* framework functions at runtime. This avoids a hard link-time dependency,
* making the library more portable and resilient if these frameworks change.
*/
static void initialize_macos_apis(void) {
// We don't need to link against these frameworks, which makes building simpler.
infix/src/jit/executor.c view on Meta::CPAN
return ExceptionContinueSearch; // Unreachable
}
#if defined(INFIX_ARCH_X64)
// Internal: Populates and registers SEH metadata for a Windows x64 JIT block.
static void _infix_register_seh_windows_x64(infix_executable_t * exec,
infix_executable_category_t category,
uint32_t prologue_size,
uint32_t epilogue_offset) {
// metadata_ptr starts after the machine code.
uint8_t * metadata_base = (uint8_t *)exec->rw_ptr + exec->size;
// RUNTIME_FUNCTION (PDATA) - Must be 4-byte aligned.
RUNTIME_FUNCTION * rf = (RUNTIME_FUNCTION *)_infix_align_up((size_t)metadata_base, 4);
// UNWIND_INFO (XDATA) - Follows PDATA.
UNWIND_INFO * ui = (UNWIND_INFO *)_infix_align_up((size_t)(rf + 1), 2);
ui->Version = 1;
ui->Flags = 0;
infix/src/jit/executor.c view on Meta::CPAN
// Initial state: CFA = rsp + 8, rip at CFA - 8
*p++ = 0x0c;
*p++ = 0x07;
*p++ = 0x08;
*p++ = 0x90;
*p++ = 0x01;
while ((size_t)(p - eh) < cie_size)
*p++ = 0;
// FDE
uint8_t * fde_start = eh + cie_size;
p = fde_start;
*(uint32_t *)p = (uint32_t)(fde_size - 4);
p += 4;
*(uint32_t *)p = (uint32_t)(p - eh);
p += 4; // back-offset
*(void **)p = exec->rx_ptr;
p += 8;
*(uint64_t *)p = (uint64_t)exec->size;
p += 8;
*p++ = 0; // aug data len
infix/src/jit/executor.c view on Meta::CPAN
// CIE Instructions: Initial state
// DW_CFA_def_cfa sp, 0
*p++ = 0x0c;
*p++ = 31;
*p++ = 0;
while ((size_t)(p - eh) < cie_size)
*p++ = 0;
// FDE (Frame Description Entry)
uint8_t * fde_start = eh + cie_size;
p = fde_start;
*(uint32_t *)p = (uint32_t)(fde_size - 4);
p += 4; // length
*(uint32_t *)p = (uint32_t)(p - eh);
p += 4; // cie_pointer (back-offset)
*(void **)p = exec->rx_ptr;
p += 8; // pc_begin (absolute)
*(uint64_t *)p = (uint64_t)exec->size;
p += 8; // pc_range (absolute)
*p++ = 0; // aug data len
infix/src/jit/executor.c view on Meta::CPAN
FlushInstructionCache(GetCurrentProcess(), exec->rw_ptr, exec->size);
#elif defined(INFIX_OS_MACOS)
// Use the Apple-specific API if available (required for Apple Silicon correctness)
if (g_macos_apis.sys_icache_invalidate)
g_macos_apis.sys_icache_invalidate(exec->rw_ptr, exec->size);
else
__builtin___clear_cache((char *)exec->rw_ptr, (char *)exec->rw_ptr + exec->size);
#elif defined(INFIX_ARCH_AARCH64)
// Robust manual cache clearing for AArch64 Linux/BSD.
// We clean the D-cache to point of unification and invalidate the I-cache.
uintptr_t start = (uintptr_t)exec->rw_ptr;
uintptr_t end = start + exec->size;
uintptr_t ctr_el0;
__asm__ __volatile__("mrs %0, ctr_el0" : "=r"(ctr_el0));
// D-cache line size is in bits [19:16] as log2 of number of words.
uintptr_t d_line_size = 4 << ((ctr_el0 >> 16) & 0xf);
for (uintptr_t addr = start & ~(d_line_size - 1); addr < end; addr += d_line_size)
__asm__ __volatile__("dc cvau, %0" ::"r"(addr) : "memory");
__asm__ __volatile__("dsb ish" ::: "memory");
// I-cache line size is in bits [3:0] as log2 of number of words.
uintptr_t i_line_size = 4 << (ctr_el0 & 0xf);
for (uintptr_t addr = start & ~(i_line_size - 1); addr < end; addr += i_line_size)
__asm__ __volatile__("ic ivau, %0" ::"r"(addr) : "memory");
__asm__ __volatile__("dsb ish\n\tisb" ::: "memory");
#else
// Use the GCC/Clang built-in for other platforms.
__builtin___clear_cache((char *)exec->rw_ptr, (char *)exec->rw_ptr + exec->size);
#endif
bool result = false;
#if defined(INFIX_OS_WINDOWS)
// On Windows, we register SEH unwind info before making the memory executable.
lib/Affix.c view on Meta::CPAN
if (!str)
str = SvPV_nolen(type_sv);
// Promote "SV" to "@SV" so the parser sees it as a named type.
// This allows the infix parser to recognize it as a named type.
if (str && strstr(str, "SV")) {
SV * modified = sv_newmortal();
sv_setpvn(modified, "", 0);
const char * p = str;
const char * start = str;
while ((p = strstr(p, "SV"))) {
// Check boundaries: Ensure we match whole word "SV"
// Start boundary: Beginning of string OR prev char is not alnum/_/@
bool start_ok = (p == start) || (!isALNUM((unsigned char)*(p - 1)) && *(p - 1) != '_' && *(p - 1) != '@');
// End boundary: End of string OR next char is not alnum/_
bool end_ok = (p[2] == '\0') || (!isALNUM((unsigned char)p[2]) && p[2] != '_');
if (start_ok && end_ok) {
// Append everything before this match
sv_catpvn(modified, start, p - start);
// Append the named type reference
sv_catpvs(modified, "@SV");
// Advance past "SV"
p += 2;
start = p;
}
else // Not a standalone "SV", skip this occurrence
p++;
}
// Append remainder
sv_catpv(modified, start);
// only return modified string if we actually changed something
if (SvCUR(modified) > strlen(str))
return SvPV_nolen(modified);
}
return str;
}
int64_t affix_perl_shim_sv_to_sint64(pTHX_ void * sv_raw) { return SvIVX((SV *)sv_raw); }
double affix_perl_shim_sv_to_double(pTHX_ void * sv_raw) { return SvNVX((SV *)sv_raw); }
lib/Affix.c view on Meta::CPAN
safefree(entry);
hv_delete_ent(MY_CXT.lib_registry, newSVpvn(lookup_path, strlen(lookup_path)), G_DISCARD, 0);
}
}
}
warn("Failed to locate symbol '%s'", symbol_name_str ? symbol_name_str : "(null)");
XSRETURN_UNDEF;
}
}
// Argument shifting (Determine where signature starts)
SV * args_sv = nullptr;
SV * ret_sv = nullptr;
SV * sig_sv = nullptr;
bool explicit_args = false; // true if using [Args] => Ret format
if (is_raw_ptr_target) {
// Mode A: wrap($ptr, [Args], Ret) -> items=3
// Mode B: wrap($ptr, "Signature") -> items=2
if (items == 3) {
args_sv = ST(1);
lib/Affix.c view on Meta::CPAN
for (SSize_t i = 0; i < num_args; ++i) {
SV ** type_sv_ptr = av_fetch(args_av, i, 0);
if (!type_sv_ptr)
continue;
const char * arg_sig = _get_string_from_type_obj(aTHX_ * type_sv_ptr);
if (!arg_sig)
croak("Invalid type object in signature");
strcat(signature_buf, arg_sig);
// Logic to prevent adding commas around ';', which denotes VarArgs start
if (i < num_args - 1) {
if (strEQ(arg_sig, ";"))
continue;
SV ** next_sv_ptr = av_fetch(args_av, i + 1, 0);
if (next_sv_ptr) {
const char * next_sig = _get_string_from_type_obj(aTHX_ * next_sv_ptr);
if (next_sig && strEQ(next_sig, ";"))
continue;
}
strcat(signature_buf, ",");
lib/Affix.c view on Meta::CPAN
}
}
else if (!SvOK(sv))
*(void **)p = nullptr;
else
croak("Argument for a callback must be a code reference or undef.");
}
static SV * _format_parse_error(pTHX_ const char * context_msg, const char * signature, infix_error_details_t err) {
STRLEN sig_len = strlen(signature);
int radius = 20;
size_t start = (err.position > radius) ? (err.position - radius) : 0;
size_t end = (err.position + radius < sig_len) ? (err.position + radius) : sig_len;
const char * start_indicator = (start > 0) ? "... " : "";
const char * end_indicator = (end < sig_len) ? " ..." : "";
int start_indicator_len = (start > 0) ? 4 : 0;
char snippet[128];
snprintf(
snippet, sizeof(snippet), "%s%.*s%s", start_indicator, (int)(end - start), signature + start, end_indicator);
char pointer[128];
int caret_pos = err.position - start + start_indicator_len;
snprintf(pointer, sizeof(pointer), "%*s^", caret_pos, "");
return sv_2mortal(newSVpvf("Failed to parse signature %s:\n\n %s\n %s\n\nError: %s (at position %zu)",
context_msg,
snippet,
pointer,
err.message,
err.position));
}
XS_INTERNAL(Affix_Lib_as_string) {
dVAR;
lib/Affix.c view on Meta::CPAN
elem_type = infix_type_create_primitive(INFIX_PRIMITIVE_UINT8);
}
if (elem_size == 0)
croak("Cannot index into zero-sized type");
void * target = (char *)pin->pointer + (index * elem_size);
if (elem_type->category == INFIX_TYPE_STRUCT || elem_type->category == INFIX_TYPE_UNION) {
HV * hv = newHV();
SV * rv = newRV_noinc(MUTABLE_SV(hv));
sv_bless(rv, gv_stashpv("Affix::Live", GV_ADD));
// We might need to pass the owner here too, but let's start with pins
_populate_hv_from_c_struct(aTHX_ nullptr, hv, elem_type, target, true, pin->owner_sv ? pin->owner_sv : ST(0));
ST(0) = sv_2mortal(rv);
}
else if (elem_type->category == INFIX_TYPE_ARRAY) {
// Return a new Affix::Pointer for this sub-array (Live view)
Affix_Pin * new_pin;
Newxz(new_pin, 1, Affix_Pin);
new_pin->pointer = target;
new_pin->managed = false;
lib/Affix.pod view on Meta::CPAN
=over
=item 1. B<Prevent Mangling:> Wrap your exported functions in C<extern "C"> to ensure they have predictable names.
extern "C" {
int add(int a, int b) { return a + b; }
}
=item 2. B<Or Use Mangled Names:> If you cannot change the C++ source, you must look up the exact mangled name (e.g., C<_Z3addii>) using tools like C<nm> or C<objdump>, and bind to that.
=item 3. B<Object Methods:> Calling an object's method requires passing the object instance pointer (the C<this> pointer) as the first argument. Use the C<ThisCall( ... )> wrapper around your callback/signature to automatically insert C<Pointer[Void]...
=back
=head2 Rust
Rust does not use the C ABI by default. You must explicitly instruct the compiler to format the function correctly.
=over
=item 1. B<Exporting:> Use C<#[no_mangle]> and C<pub extern "C">.
lib/Affix.pod view on Meta::CPAN
=head2 Advanced Debugging
=head3 C<set_destruct_level( $level )>
Sets the internal C<PL_perl_destruct_level> variable.
When testing XS/FFI code for memory leaks using tools like Valgrind or AddressSanitizer, you often want Perl to
meticulously clean up all global memory during its destruction phase (otherwise the leak checker will be flooded with
false-positive "leaks" that are actually just memory Perl intentionally leaves to the OS to reclaim).
# Call this at the start of your script when running under Valgrind
set_destruct_level(2);
=head1 COMPANION MODULES
Affix ships with two powerful companion modules to streamline your FFI development:
=over
=item * L<B<Affix::Wrap>|Affix::Wrap>: Parses C/C++ headers using the Clang AST to automatically generate Affix bindings for entire libraries.
lib/Affix.pod view on Meta::CPAN
=back
=head1 THREAD SAFETY & CONCURRENCY
Affix bridges Perl (a single-threaded interpreter, generally) with libraries that may be multi-threaded. This creates
potential hazards that you must manage.
=head2 1. Initialization Phase vs. Execution Phase
Functions that modify Affix's global state are B<not thread-safe>. You must perform all definitions in the main thread
before starting any background threads or loops in the library.
Unsafe operations that you should never call from Callbacks or in a threaded context:
=over
=item * C<affix( ... )> - Binding new functions.
=item * C<typedef( ... )> - Registering new types.
=back
lib/Affix/Wrap.pm view on Meta::CPAN
method affix_type { $type->affix_type }
method affix { $type->affix }
}
class #
Affix::Wrap::Entity {
field $name : reader : param //= '';
field $doc : reader : param //= ();
field $file : reader : param //= '';
field $line : reader : param //= 0;
field $end_line : reader : param //= 0;
field $start_offset : reader : param //= 0;
field $end_offset : reader : param //= 0;
field $is_merged : reader = 0;
field $doc_data = undef;
method mark_merged { $is_merged = 1 }
method _base($p) { return '' unless defined $p; $p =~ s{^.*[/\\]}{}; return $p }
method describe {
return sprintf '[%s] %s (%s:%d)', __CLASS__ =~ s/^Affix::Wrap:://r, $name, $self->_base($file), $line;
}
lib/Affix/Wrap.pm view on Meta::CPAN
my ( $stdout, $stderr, $exit ) = Capture::Tiny::capture { system(@cmd); };
if ( $exit != 0 ) { die "Clang Error:\n$stderr"; }
if ( $stdout =~ /^.*?(\{.*)/s ) { $stdout = $1; }
my $ast = JSON::PP::decode_json($stdout);
my @objects;
$self->_walk( $ast, \@objects, $ep_abs );
$self->_scan_macros_fallback( \@objects );
$self->_merge_typedefs( \@objects );
$self->_wrap_named_types( \@objects );
#~ @objects = sort { ( $a->file cmp $b->file ) || ( $a->start_offset <=> $b->start_offset ) } @objects;
@objects;
}
method _walk( $node, $acc, $current_file ) {
return unless ref $node eq 'HASH';
my $kind = $node->{kind} // 'Unknown';
my $node_file = $self->_get_node_file($node);
if ($node_file) {
$current_file = $self->_normalize($node_file);
$last_seen_file = $current_file;
lib/Affix/Wrap.pm view on Meta::CPAN
my ( $s, $e, $l, $el ) = $self->_meta($n);
my $val = $self->_extract_macro_val( $n, $f );
push @$acc,
Affix::Wrap::Macro->new(
name => $n->{name},
file => $f,
line => $l,
end_line => $el,
value => $val,
doc => $self->_extract_doc( $f, $s ),
start_offset => $s,
end_offset => $e
);
}
method _typedef( $n, $acc, $f ) {
my ( $s, $e, $l, $el ) = $self->_meta($n);
push @$acc,
Affix::Wrap::Typedef->new(
name => $n->{name},
file => $f,
line => $l,
end_line => $el,
underlying => Affix::Wrap::Type->parse( $n->{type}{qualType} ),
doc => $self->_doc_w_trail( $f, $s, $e ),
start_offset => $s,
end_offset => $e
);
}
method _record( $n, $acc, $f ) {
my ( $s, $e, $l, $el ) = $self->_meta($n);
my $m_list = $self->_extract_members( $n, $f );
return unless ( $n->{name} || @$m_list );
my $tag = $n->{tagUsed} // 'struct';
push @$acc,
Affix::Wrap::Struct->new(
name => $n->{name} // '(anonymous)',
tag => $tag,
file => $f,
line => $l,
end_line => $el,
members => $m_list,
doc => $self->_doc_w_trail( $f, $s, $e ),
start_offset => $s,
end_offset => $e
);
}
method _extract_members( $n, $f ) {
my @members;
return \@members unless $n->{inner};
my @pending_anonymous_records;
for my $child ( @{ $n->{inner} } ) {
my $kind = $child->{kind} // '';
lib/Affix/Wrap.pm view on Meta::CPAN
}
}
push @$acc,
Affix::Wrap::Enum->new(
name => $n->{name} // '(anonymous)',
file => $f,
line => $l,
end_line => $el,
constants => \@c,
doc => $self->_doc_w_trail( $f, $s, $e ),
start_offset => $s,
end_offset => $e
);
}
method _var( $n, $acc, $f ) {
my ( $s, $e, $l, $el ) = $self->_meta($n);
push @$acc,
Affix::Wrap::Variable->new(
name => $n->{name},
file => $f,
line => $l,
end_line => $el,
type => Affix::Wrap::Type->parse( $n->{type}{qualType} ),
doc => $self->_doc_w_trail( $f, $s, $e ),
start_offset => $s,
end_offset => $e
);
}
method _func( $n, $acc, $f ) {
return if ( $n->{storageClass} // '' ) eq 'static';
my ( $s, $e, $l, $el ) = $self->_meta($n);
my $ret_str = $n->{type}{qualType};
$ret_str =~ s/\(.*\)//;
my $ret_obj = Affix::Wrap::Type->parse($ret_str);
lib/Affix/Wrap.pm view on Meta::CPAN
push @$acc,
Affix::Wrap::Function->new(
name => $n->{name},
mangled_name => $n->{mangledName},
file => $f,
line => $l,
end_line => $el,
ret => $ret_obj,
args => \@args,
doc => $self->_doc_w_trail( $f, $s, $e ),
start_offset => $s,
end_offset => $e
);
}
method _get_content($f) {
my $abs = $self->_normalize($f);
return $file_cache->{$abs} if exists $file_cache->{$abs};
if ( -e $abs ) { return $file_cache->{$abs} = Path::Tiny::path($abs)->slurp_utf8; }
return '';
}
lib/Affix/Wrap.pm view on Meta::CPAN
$val =~ s/\/\*.*?\*\///g;
$val =~ s/^\s+|\s+$//g;
push @$acc,
Affix::Wrap::Macro->new(
name => $name,
file => $f,
line => $line,
end_line => $line,
value => $val,
doc => $self->_extract_doc( $f, $off ),
start_offset => $off,
end_offset => $end
);
}
}
}
method _merge_typedefs($objs) {
my @tds = grep { ref($_) eq 'Affix::Wrap::Typedef' } @$objs;
for my $td (@tds) {
next if $td->is_merged;
lib/Affix/Wrap.pm view on Meta::CPAN
( ref($_) eq 'Affix::Wrap::Enum' || ref($_) eq 'Affix::Wrap::Struct' ) &&
( abs( $_->end_line - $td->line ) <= 2 || abs( $_->end_line - $td->end_line ) <= 2 )
} @$objs;
if ($child) {
my $new = Affix::Wrap::Typedef->new(
name => $td->name,
underlying => $child,
file => $td->file,
line => $td->line,
end_line => $child->end_line,
doc => $td->doc // $child->doc // $self->_extract_doc( $td->file, $td->start_offset ),
start_offset => $td->start_offset,
end_offset => $td->end_offset
);
for ( my $i = 0; $i < @$objs; $i++ ) {
if ( $objs->[$i] == $td ) { $objs->[$i] = $new; last; }
}
$child->mark_merged();
}
}
@$objs = grep { !$_->is_merged } @$objs;
}
lib/Affix/Wrap.pm view on Meta::CPAN
# If there's already a typedef for this name, skip it
next if grep { $_ isa Affix::Wrap::Typedef && $_->name eq $node->name } @$objs;
my $new = Affix::Wrap::Typedef->new(
name => $node->name,
underlying => $node,
file => $node->file,
line => $node->line,
end_line => $node->end_line,
doc => $node->doc,
start_offset => $node->start_offset,
end_offset => $node->end_offset
);
$node->mark_merged();
$objs->[$i] = $new;
}
}
}
method _get_triple {
my $arch = $Config{archname} =~ /aarch64|arm64/i ? 'aarch64' : $Config{archname} =~ /x64|x86_64/i ? 'x86_64' : 'i686';
lib/Affix/Wrap.pm view on Meta::CPAN
method parse( $entry_point, $ids //= [] ) {
my @objs;
for my $f (@$project_files) {
my $abs = $self->_normalize($f);
next unless length $abs;
next unless $self->_is_valid_file($abs);
if ( $f =~ /\.h(pp|xx)?$/i ) { $self->_scan( $f, \@objs ); $self->_scan_funcs( $f, \@objs ); }
else { $self->_scan_funcs( $f, \@objs ); }
}
@objs = sort { ( $a->file cmp $b->file ) || ( $a->start_offset <=> $b->start_offset ) } @objs;
@objs;
}
method _read($f) {
my $abs = $self->_normalize($f);
return $file_cache->{$abs} if exists $file_cache->{$abs};
return $file_cache->{$abs} = Path::Tiny::path($f)->slurp_utf8;
}
method _scan( $f, $acc ) {
lib/Affix/Wrap.pm view on Meta::CPAN
$val =~ s/\/\*.*?\*\///g;
$val =~ s/^\s+|\s+$//g;
push @$acc,
Affix::Wrap::Macro->new(
name => $name,
value => $val,
file => $f,
line => $self->_ln( $c, $s ),
end_line => $self->_ln( $c, $e ),
doc => $self->_doc( $c, $s ),
start_offset => $s,
end_offset => $e
);
}
# Structs
while ( $c =~ /typedef\s+struct\s*(?:\w+\s*)?(\{(?:[^{}]++|(?1))*\})\s*(\w+)\s*;/gs ) {
my $s = $-[0];
my $e = $+[0];
my $mem = $self->_mem( substr( $1, 1, -1 ) );
my $struct = Affix::Wrap::Struct->new(
name => '',
tag => 'struct',
members => $mem,
file => $f,
line => $self->_ln( $c, $s ),
end_line => $self->_ln( $c, $e ),
doc => undef,
start_offset => $s,
end_offset => $e
);
push @$acc,
Affix::Wrap::Typedef->new(
name => $2,
underlying => $struct,
file => $f,
line => $self->_ln( $c, $s ),
end_line => $self->_ln( $c, $e ),
doc => $self->_doc( $c, $s ),
start_offset => $s,
end_offset => $e
);
}
# Enums (typedef)
while ( $c =~ /typedef\s+enum\s*(?:\w+\s*)?(\{(?:[^{}]++|(?1))*\})\s*(\w+)\s*;/gs ) {
my $s = $-[0];
my $e = $+[0];
my $enum = Affix::Wrap::Enum->new(
name => '',
constants => $self->_enum_consts( substr( $1, 1, -1 ) ),
file => $f,
line => $self->_ln( $c, $s ),
end_line => $self->_ln( $c, $e ),
doc => undef,
start_offset => $s,
end_offset => $e
);
push @$acc,
Affix::Wrap::Typedef->new(
name => $2,
underlying => $enum,
file => $f,
line => $self->_ln( $c, $s ),
end_line => $self->_ln( $c, $e ),
doc => $self->_doc( $c, $s ),
start_offset => $s,
end_offset => $e
);
}
# Enums (standard)
while ( $c =~ /enum\s+(\w+)\s*(\{(?:[^{}]++|(?2))*\})\s*;/gs ) {
my $s = $-[0];
my $e = $+[0];
my $enum = Affix::Wrap::Enum->new(
name => $1,
constants => $self->_enum_consts( substr( $2, 1, -1 ) ),
file => $f,
line => $self->_ln( $c, $s ),
end_line => $self->_ln( $c, $e ),
doc => $self->_doc( $c, $s ),
start_offset => $s,
end_offset => $e
);
push @$acc,
Affix::Wrap::Typedef->new(
name => $1,
underlying => $enum,
file => $f,
line => $self->_ln( $c, $s ),
end_line => $self->_ln( $c, $e ),
doc => $self->_doc( $c, $s ),
start_offset => $s,
end_offset => $e
);
}
# Global variables
while ( $c
=~ /^\s*extern\s+((?:const\s+|unsigned\s+|struct\s+|[\w:<>]+(?:\s*::\s*[\w:<>]+)*\s*\*?\s*)+?)([\s\*]+)([a-zA-Z_]\w*(?:\[.*?\])?)\s*(?:=\s*[^;]+)?\s*;\s*(?:\/\/([^\r\n]*))?/gm
) {
my $s = $-[0];
my $e = $+[0];
lib/Affix/Wrap.pm view on Meta::CPAN
$doc = defined $doc ? "$doc\n$trail" : $trail;
}
push @$acc,
Affix::Wrap::Variable->new(
name => $name,
type => Affix::Wrap::Type->parse($type_str),
file => $f,
line => $self->_ln( $c, $s ),
end_line => $self->_ln( $c, $e ),
doc => $doc,
start_offset => $s,
end_offset => $e
);
}
# Typedefs
while ( $c =~ /typedef\s+(?!struct\s*(?:\w+\s*)?\{|enum\s*(?:\w+\s*)?\{)(.+?)\s*;/gs ) {
my $content = $1;
my $s = $-[0];
my $e = $+[0];
$content =~ s/\s+/ /g;
lib/Affix/Wrap.pm view on Meta::CPAN
}
my $coderef = Affix::Wrap::Type::CodeRef->new( ret => $ret, params => \@args );
push @$acc,
Affix::Wrap::Typedef->new(
name => $name,
underlying => $coderef,
file => $f,
line => $self->_ln( $c, $s ),
end_line => $self->_ln( $c, $e ),
doc => $self->_doc( $c, $s ),
start_offset => $s,
end_offset => $e
);
}
elsif ( $content =~ /^(.+?)([\s\*]+)([a-zA-Z_]\w*(?:\[.*?\])?)$/ ) {
my ( $type_str, $sep, $name ) = ( $1, $2, $3 );
$type_str .= $sep;
if ( $name =~ s/(\[.*\])$// ) { $type_str .= $1; }
push @$acc,
Affix::Wrap::Typedef->new(
name => $name,
underlying => Affix::Wrap::Type->parse($type_str),
file => $f,
line => $self->_ln( $c, $s ),
end_line => $self->_ln( $c, $e ),
doc => $self->_doc( $c, $s ),
start_offset => $s,
end_offset => $e
);
}
}
}
method _enum_consts($body) {
my @cs;
my $v = 0;
for ( split /,/, $body ) {
lib/Affix/Wrap.pm view on Meta::CPAN
push @$acc,
Affix::Wrap::Function->new(
name => $func_name,
mangled_name => $func_name,
ret => $ret_obj,
args => \@args,
file => $f,
line => $self->_ln( $c, $s ),
end_line => $self->_ln( $c, $e ),
doc => $self->_doc( $c, $s ),
start_offset => $s,
end_offset => $e
);
}
}
method _mem($b) {
my @m;
my $pending_doc = '';
my $clean = sub ($t) {
$t =~ s/^\s*\/\*\*?//mg;
lib/Affix/Wrap.pod view on Meta::CPAN
types => {
'sqlite3' => 'Pointer[Void]' # Custom override
}
);
# Option 1: Instantly wrap and inject into the current namespace
# You may call functions exported by the lib immediately
$wrapper->wrap('libsqlite3.so', __PACKAGE__);
# Option 2: Generate a standalone Perl module file to disk
# This should get you started on a library wrapper you'll eventually put on CPAN
$wrapper->generate('libsqlite3.so', 'My::SQLite', 'lib/My/SQLite.pm');
=head1 DESCRIPTION
C<Affix::Wrap> is a frictionless binding generator that bridges C/C++ header files and L<Affix>. It parses headers to
extract functions, structs, enums, typedefs, macros, and global variables, automatically converting this information
into L<Affix> definitions.
It is designed to facilitate two primary developer workflows:
lib/Affix/Wrap.pod view on Meta::CPAN
Parses the project files and returns a list of Node objects (see B<Data Model> below). Use this if you want to inspect
the C header structure or generate code strings for a static Perl module.
The nodes are sorted by file name and line number to ensure deterministic output order.
=over
=item C<$entry_point>
Optional. The specific file to start parsing from. Defaults to the first file in C<project_files>.
=back
=head1 Data Model
The C<parse()> method returns a list of objects inheriting from C<Affix::Wrap::Entity>.
All nodes provide at least two key methods:
=over
t/005_varargs.t view on Meta::CPAN
} Point;
/*
* Sums values based on format string.
* i: int
* d: double
* P: Point {int, int}
*/
DLLEXPORT int var_sum(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
int total = 0;
while (*fmt) {
switch (*fmt++) {
case 'i': total += va_arg(ap, int); break;
case 'd': total += (int)va_arg(ap, double); break;
case 's': {
char* s = va_arg(ap, char*);
total += s ? strlen(s) : 0;
break;
}