Affix

 view release on metacpan or  search on metacpan

CONTRIBUTING.md  view on Meta::CPAN

    ```bash
    # For a new feature:
    git checkout -b feature/add-riscv-support
    # For a bug fix:
    git checkout -b fix/struct-classification-bug
    ```

4.  **Make Your Changes**: Write your code. Please adhere to the existing coding style and add comments to new or complex logic.
5.  **Test Your Changes**: A pull request is far more likely to be accepted if it includes tests. If you add new functionality, please add a corresponding test case.
6.  **Update the Changelog**: Add an entry to the `[Unreleased]` section of `CHANGELOG.md` describing your change.
7.  **Submit a Pull Request**: Push your branch to your fork and open a pull request against the `main` branch of the original repository. Please provide a clear description of your changes and link to the relevant issue (e.g., `Fixes #123`).

Changes.md  view on Meta::CPAN

- `SAVEVPTR`/`SAVEDESTRUCTOR_X` 'fix' broke thread safety. When a second thread or fiber entered the same Affix-generated XSUB, its `SAVEVPTR` captured the first fiber's temp arena as "old", so on scope exit the second fiber restored a dangling point...

## [v1.2.4] - 2026-08-15

Plugging leaks...

### Fixed

- Use `SAVEVPTR` and `SAVEDESTRUCTOR_X` to swap out arenas to fix leaky allocator in situations where tons of structs are passed in a list and need to be marshalled in only one direction
- Casting or binding an aggregate (`Affix::cast`, member pins) no longer leaks: member pins borrowed the freshly created parent hash/array as their lifeline, forming a strong reference cycle that Perl's refcounting cannot collect, so the whole pin tr...
- Passing a union to a wrapped call no longer segfaults: the argument sync read back *every* union member, and reading an inactive pointer/string member dereferenced the active member's float bytes as a C string pointer. Deep writes now skip members ...
- The library probe in `Affix::Platform::Unix` (`_findLib_gcc`) no longer prints linker errors (`undefined reference to WinMain`/`main`) while searching: it probes with `-shared`, which needs no entry point.
- Bitfields inside `Struct[...]` are no longer read or written out of bounds: `member->offset` now points at the storage unit base (with `bit_offset` relative to the unit) instead of the bitfield's own byte, so the unit-sized load/store in `push_stru...
- Reading and writing packed struct members (and pinned primitives) no longer uses unaligned native loads/stores: the dispatch vtables, bitfield vtables, pull handlers, and push handlers now round-trip through `memcpy`, which is safe on strict-alignm...
- Passing a wide string (`WString()`, i.e. `*wchar_t`) to a wrapped function now works on all platforms instead of croaking `Don't know how to handle this type of scalar as a pointer argument yet` on non-Windows systems, where the wide-string push op...
- Returning a `WString` no longer crashes: the wide-string pull handler called `SvGROW` on an uninitialized target SV, faulting before any buffer was allocated.
- [infix] Passing a 5-7 byte `Struct[...]` by value to a wrapped function no longer drops the trailing members on ARM64. The forward trampoline emitted a 32-bit register load unless the struct was exactly 8 bytes, so a `Struct[ arr => Array[2, UInt16...

## [v1.2.3] - 2026-08-08

### Fixed

Changes.md  view on Meta::CPAN

## [v1.0.8] - 2026-03-02

This release introduces a modernization of pointer handling, turning pins into first-class objects with native indexing support. In other words, you can now use `$ptr->[$n]` to access the nth element.

Support for passing 128bit integers around is now complete. Additionally, functions expecting an enumeration now accept the string name of a constant; `state('PLASMA')` is the same as `state(PLASMA)` where `state` expects values defined like this: `t...

### Added

- Added support for native Perl array indexing on pointers and arrays. You can now use `$ptr->[$i]` to read or write memory at an offset without manual casting.
- New `Affix::Pointer` objects for structs and unions now allow direct field access like `$ptr->{field}` without explicit casting to `LiveStruct`.
- Recursive Liveness: Unified access and `LiveStruct` now work recursively. Accessing a nested struct member returns a live view or pointer tied to the original memory block.
- All pointers returned by `malloc`, `calloc`, `cast`, etc., are now blessed into the `Affix::Pointer` class, which provides several new methods:
    - `address()`: Returns the virtual memory address.
    - `type()`: Returns the L<infix> signature of the pointer.
    - `element_type()`: Returns the signature of the pointed-to elements.
    - `count()`: Returns the element count for Arrays, or byte size for Void pointers.
    - `size()`: Returns the total allocated size for managed pointers.
    - `cast($type)`: Reinterprets the pointer.
    - `attach_destructor($destructor, [$lib])`: Attaches a custom C cleanup routine to the pointer.
- Added `attach_destructor( $pin, $destructor, [$lib] )` to allow attaching custom C cleanup routines to managed pointers.
- Improved `VarArgs` support to automatically promote Perl strings to `char*`.

LICENSE  view on Meta::CPAN

(1)  You are permitted to use the Standard Version and create and use
Modified Versions for any purpose without restriction, provided that
you do not Distribute the Modified Version.


Permissions for Redistribution of the Standard Version

(2)  You may Distribute verbatim copies of the Source form of the
Standard Version of this Package in any medium without restriction,
either gratis or for a Distributor Fee, provided that you duplicate
all of the original copyright notices and associated disclaimers.  At
your discretion, such verbatim copies may or may not include a
Compiled form of the Package.

(3)  You may apply any bug fixes, portability changes, and other
modifications made available from the Copyright Holder.  The resulting
Package will still be considered the Standard Version, and as such
will be subject to the Original License.


Distribution of Modified Versions of the Package as Source 

README.md  view on Meta::CPAN

    ```perl
    my $addr = address($some_ptr);
    pin my $val, $addr, Int;
    ```

- **pin( $var, $existing\_pin )**

    Clones the binding from an existing pin (copies the address and type).

    ```perl
    pin my $copy, $original_pin;
    ```

## `unpin( $var )`

Removes the magic applied by `pin`. The variable retains its last value but is no longer linked to C memory.

# TYPE SYSTEM

Map C types to Perl with a rich, built-in vocabulary. Affix signatures are built using helper functions that map
precisely to C types. These are exported by default, or can be imported explicitly using the `:types` tag.

README.md  view on Meta::CPAN


Allocates zero-initialized memory for `$count` elements of `$size`. Returns a `Pointer[Void]` pin.

```perl
my $ptr = calloc( 10, sizeof(Int) );
my $arr = cast( $ptr, Array[Int, 10] );
```

### `realloc( $ptr, $new_size )`

Resizes the memory area pointed to by `$ptr` to `$new_size` bytes. The original pin is updated automatically
in-place.

```
$ptr = realloc( $ptr, 2048 );
```

### `strdup( $string )`

Allocates managed memory and copies the Perl string (along with a `NULL` terminator) into it. Returns a managed
`Pointer[Char]` pin.

infix/LICENSE-A2  view on Meta::CPAN

(1)  You are permitted to use the Standard Version and create and use
Modified Versions for any purpose without restriction, provided that
you do not Distribute the Modified Version.


Permissions for Redistribution of the Standard Version

(2)  You may Distribute verbatim copies of the Source form of the
Standard Version of this Package in any medium without restriction,
either gratis or for a Distributor Fee, provided that you duplicate
all of the original copyright notices and associated disclaimers.  At
your discretion, such verbatim copies may or may not include a
Compiled form of the Package.

(3)  You may apply any bug fixes, portability changes, and other
modifications made available from the Copyright Holder.  The resulting
Package will still be considered the Standard Version, and as such
will be subject to the Original License.


Distribution of Modified Versions of the Package as Source

infix/LICENSE-CC  view on Meta::CPAN

information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.

Using Creative Commons Public Licenses

Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.

     Considerations for licensors: Our public licenses are
     intended for use by those authorized to give the public
     permission to use material in ways otherwise restricted by
     copyright and certain other rights. Our licenses are
     irrevocable. Licensors should read and understand the terms
     and conditions of the license they choose before applying it.

infix/include/infix/infix.h  view on Meta::CPAN

 */

/**
 * @brief A function pointer for a direct marshalling forward trampoline.
 *
 * This is the callable code generated by `infix_forward_create_direct`. It takes
 * an array of `void*` pointers that point directly to the language-specific
 * objects (e.g., `SV*` in Perl, `PyObject*` in Python).
 *
 * @param return_value_ptr A pointer to a buffer to receive the C function's return value.
 * @param lang_objects_array An array of `void*` pointers to the original language objects.
 */
typedef void (*infix_direct_cif_func)(void *, void **);

/**
 * @brief A union to hold any primitive value returned by a scalar marshaller.
 *
 * Since a C function can only have one return type, a marshaller for primitive
 * types (`infix_marshaller_fn`) returns this union. The JIT-compiled code will
 * know which member of the union to access based on the argument's C type.
 */

infix/include/infix/infix.h  view on Meta::CPAN

 * @param type A pointer to the `infix_type` object describing the C aggregate. The
 *             marshaller can introspect this type to determine field names, offsets,
 *             and member types.
 */
typedef void (*infix_aggregate_marshaller_fn)(void * source_object, void * dest_buffer, const infix_type * type);

/**
 * @brief A function pointer for a "write-back" handler for out/in-out parameters.
 *
 * This function is called by the JIT trampoline *after* the native C function has
 * returned. Its purpose is to update the original language object with any changes
 * made to the C data by the function.
 *
 * @param source_object A `void*` pointer to the original language object that was passed in.
 * @param c_data_ptr A pointer to the (potentially modified) C data buffer that was passed
 *                   to the C function.
 * @param type A pointer to the `infix_type` object describing the C data.
 */
typedef void (*infix_writeback_fn)(void * source_object, void * c_data_ptr, const infix_type * type);

/**
 * @brief A struct containing all the necessary handlers for a single function argument.
 *
 * For each argument, a language binding provides an instance of this struct. Based on

infix/src/arch/x64/abi_sysv_x64.c  view on Meta::CPAN

 * @param buf The code buffer.
 * @param layout The call frame layout.
 * @return `INFIX_SUCCESS`.
 */
static infix_status generate_forward_prologue_sysv_x64(code_buffer * buf, infix_call_frame_layout * layout) {
    // Standard Function Prologue
    emit_push_reg(buf, RBP_REG);              // push rbp
    emit_mov_reg_reg(buf, RBP_REG, RSP_REG);  // mov rbp, rsp
    // Save Callee-Saved Registers
    // We will use these registers to store our context (target_fn, ret_ptr, args_ptr)
    // across the native function call, so we must save their original values first.
    emit_push_reg(buf, R12_REG);  // push r12
    emit_push_reg(buf, R13_REG);  // push r13
    emit_push_reg(buf, R14_REG);  // push r14
    emit_push_reg(buf, R15_REG);  // push r15
    // Move Trampoline Arguments to Persistent Registers
    if (layout->target_fn == nullptr) {  // Unbound trampoline
        // The trampoline is called with (target_fn, ret_ptr, args_ptr) in RDI, RSI, RDX.
        // We move these into our saved callee-saved registers to protect them.
        emit_mov_reg_reg(buf, R12_REG, RDI_REG);  // r12 = target_fn
        emit_mov_reg_reg(buf, R13_REG, RSI_REG);  // r13 = ret_ptr

infix/src/arch/x64/abi_sysv_x64.c  view on Meta::CPAN

 * @brief Stage 4 (Reverse): Generates the code to call the high-level C dispatcher function.
 * @details Emits code to load the dispatcher's arguments into the correct registers
 *          according to the System V ABI, then calls the dispatcher.
 *
 *          The C dispatcher's signature is:
 *          `void fn(infix_reverse_t* context, void* return_value_ptr, void** args_array)`
 *
 *          The generated code performs the following argument setup:
 *          1. `RDI` (Arg 1): The `context` pointer (a 64-bit immediate).
 *          2. `RSI` (Arg 2): The pointer to the return value buffer. This is either a
 *             pointer to local stack space, or the original pointer passed by the
 *             caller in RDI if the function returns a large struct by reference.
 *          3. `RDX` (Arg 3): The pointer to the `args_array` on the local stack.
 *          4. The address of the dispatcher function itself is loaded into a scratch
 *             register (`RAX`), which is then called.
 * @param buf The code buffer.
 * @param layout The layout blueprint.
 * @param context The reverse context.
 * @return `INFIX_SUCCESS`.
 */
static infix_status generate_reverse_dispatcher_call_sysv_x64(code_buffer * buf,

infix/src/arch/x64/abi_sysv_x64.c  view on Meta::CPAN

                        emit_movsd_xmm_mem(buf, XMM1_REG, RBP_REG, layout->return_buffer_offset + 8);
                else  // INTEGER
                    emit_mov_reg_mem(buf, RDX_REG, RBP_REG, layout->return_buffer_offset + 8);
            }
        }
    }
    // Standard function epilogue: tear down stack frame and return.
    emit_mov_reg_reg(buf, RSP_REG, RBP_REG);

    if (context->return_type->category == INFIX_TYPE_VOID) {
        // Restore original RAX from its saved location at [RBP-8].
        emit_mov_reg_mem(buf, RAX_REG, RBP_REG, -8);
    }

    emit_pop_reg(buf, RBP_REG);
    emit_ret(buf);
    return INFIX_SUCCESS;
}

/**
 * @internal

infix/src/arch/x64/abi_sysv_x64.c  view on Meta::CPAN

            emit_push_reg(buf, RDX_REG);           // +8
            emit_sub_reg_imm32(buf, RSP_REG, 32);  // +32 (space for XMM0/XMM1)
            // Total stack shift: +48 bytes

            emit_movsd_mem_xmm(buf, RSP_REG, 0, XMM0_REG);

            // Set up args for write-back call
            emit_mov_reg_mem(buf, RDI_REG, R14_REG, i * sizeof(void *));

            // Arg 2 (RSI): Pointer to the C data (in our scratch space)
            // Offsets are relative to the *original* RSP of the body.
            // Since we just pushed/subbed 48 bytes, we must add 48 to reach the original frame.
            int32_t temp_offset = (int32_t)arg->location.num_regs;
            emit_lea_reg_mem(buf, RSI_REG, RSP_REG, temp_offset + 48);

            emit_mov_reg_imm64(buf, RDX_REG, (uint64_t)arg->type);

            emit_mov_reg_imm64(buf, R10_REG, (uint64_t)arg->handler->writeback_handler);
            emit_call_reg(buf, R10_REG);

            // Restore return registers
            emit_movsd_xmm_mem(buf, XMM0_REG, RSP_REG, 0);

infix/src/arch/x64/abi_win_x64.c  view on Meta::CPAN

 * @details This function emits the instructions to load the three arguments for the C
 *          dispatcher into the correct registers according to the Windows x64 ABI,
 *          then calls the dispatcher.
 *
 *          The C dispatcher's signature is:
 *          `void fn(infix_reverse_t* context, void* return_value_ptr, void** args_array)`
 *
 *          The generated code performs the following argument setup:
 *          1. `RCX` (Arg 1): The `context` pointer (a 64-bit immediate).
 *          2. `RDX` (Arg 2): The pointer to the return value buffer. This is either a
 *             pointer to local stack space, or the original pointer passed by the
 *             caller in RCX if the function returns a large struct by reference.
 *          3. `R8` (Arg 3): The pointer to the `args_array` on the local stack.
 *          4. The address of the dispatcher function itself is loaded into `R9`,
 *             which is then called.
 * @param buf The code buffer.
 * @param layout The blueprint containing stack offsets.
 * @param context The context, containing the dispatcher's address.
 * @return `INFIX_SUCCESS`.
 */
static infix_status generate_reverse_dispatcher_call_win_x64(code_buffer * buf,
                                                             infix_reverse_call_frame_layout * layout,
                                                             infix_reverse_t * context) {
    // Arg 1 (RCX): Load the `context` pointer.
    emit_mov_reg_imm64(buf, RCX_REG, (uint64_t)context);
    // Arg 2 (RDX): Load the pointer to the return value buffer.
    if (return_value_is_by_reference(context->return_type))
        // If the return is by reference, the original caller passed the destination
        // pointer in RCX. We saved it in our GPR save area (Step 1 of marshalling).
        emit_mov_reg_mem(buf, RDX_REG, RSP_REG, layout->gpr_save_area_offset + 0 * 8);
    else
        // Otherwise, the return buffer is on our local stack. Load its address.
        emit_lea_reg_mem(buf, RDX_REG, RSP_REG, layout->return_buffer_offset);

    // Arg 3 (R8): Load the address of the `args_array` on our local stack.
    emit_lea_reg_mem(buf, R8_REG, RSP_REG, layout->args_array_offset);

    if (layout->max_align >= 32)

infix/src/arch/x64/abi_win_x64.c  view on Meta::CPAN

            (context->return_type->category == INFIX_TYPE_VECTOR && context->return_type->size >= 32 &&
             !return_value_is_by_reference(context->return_type));
        if (!returning_large_vector)
            emit_vzeroupper(buf);
    }

    // Handle the return value after the dispatcher returns.
    if (context->return_type->category != INFIX_TYPE_VOID) {
        if (return_value_is_by_reference(context->return_type))
            // The return value was written directly via the hidden pointer.
            // The ABI requires this original pointer (which was in RCX) to be returned in RAX.
            emit_mov_reg_mem(buf, RAX_REG, RSP_REG, layout->gpr_save_area_offset + 0 * 8);
        else {
            // The return value is in our local buffer. Load it into the correct return register.
#if !defined(INFIX_COMPILER_MSVC)
            if (context->return_type->size == 16 && context->return_type->category == INFIX_TYPE_PRIMITIVE)
                // GCC/Clang on Windows returns 128-bit integers and long double in XMM0.
                emit_movups_xmm_mem(buf, XMM0_REG, RSP_REG, layout->return_buffer_offset);
            else
#endif
                if (context->return_type->category == INFIX_TYPE_VECTOR) {

infix/src/common/infix_internals.h  view on Meta::CPAN

 * for reporting errors from within the library. It populates the thread-local
 * `g_infix_last_error` struct. For parser errors, it generates a rich diagnostic
 * message with a code snippet.
 * @param category The general category of the error.
 * @param code The specific error code.
 * @param position For parser errors, the byte offset into the signature string where the error occurred.
 */
INFIX_INTERNAL void _infix_set_error(infix_error_category_t category, infix_error_code_t code, size_t position);
/**
 * @brief Sets the thread-local error state for a system-level error.
 * @details Located in `src/core/error.c`, this is used for errors originating from
 * the operating system, such as `dlopen` or `mmap` failures.
 * @param category The general category of the error.
 * @param code The `infix` error code that corresponds to the failure.
 * @param system_code The OS-specific error code (e.g., from `errno` or `GetLastError`).
 * @param msg An optional custom message from the OS (e.g., from `dlerror`).
 */
INFIX_INTERNAL void _infix_set_system_error(infix_error_category_t category,
                                            infix_error_code_t code,
                                            long system_code,
                                            const char * msg);

infix/src/core/error.c  view on Meta::CPAN

        // For non-parser errors, just copy the standard message.
        const char * msg = _get_error_message_for_code(code);
        _INFIX_SAFE_STRNCPY(g_infix_last_error.message, msg, sizeof(g_infix_last_error.message) - 1);
    }
}

/**
 * @internal
 * @brief Sets a detailed system error with a platform-specific error code and message.
 *
 * @details This is used for errors originating from OS-level functions like `dlopen`,
 * `mmap`, or `VirtualAlloc`. It records both the `infix` error code and the
 * underlying system error code (`errno` or `GetLastError`).
 *
 * @param category The category of the error.
 * @param code The `infix` error code that corresponds to the failure.
 * @param system_code The OS-specific error code (e.g., from `errno` or `GetLastError`).
 * @param msg An optional custom message from the OS (e.g., from `dlerror`). If `nullptr`, the default message for
 * `code` is used.
 */
void _infix_set_system_error(infix_error_category_t category,

infix/src/core/signature.c  view on Meta::CPAN

        skip_whitespace(state);
        return true;
    }
    return false;
}
/**
 * @internal
 * @brief Parses an optional named prefix, like `name: type`.
 * @details If a valid identifier is found followed by a colon, the name is returned
 * and the parser's cursor is advanced past the colon. If not, the parser state is
 * rewound to its original position (backtracking) and `nullptr` is returned.
 * @param[in,out] state The parser state.
 * @return An arena-allocated string for the name, or `nullptr` if no `name:` prefix is present.
 */
static const char * parse_optional_name_prefix(parser_state * state) {
    skip_whitespace(state);
    // Save the current position in case we need to backtrack.
    const char * p_before = state->p;
    const char * name = parse_identifier(state);
    if (name) {
        skip_whitespace(state);
        if (*state->p == ':') {  // Found "identifier:", so consume the colon and return the name.
            state->p++;
            return name;
        }
    }
    // If it wasn't a `name:`, backtrack to the original position.
    state->p = p_before;
    return nullptr;
}
/**
 * @internal
 * @brief A lookahead function to disambiguate a grouped type `(type)` from a
 *        function signature `(...) -> type`.
 *
 * @details This is a classic parser "lookahead". When the parser encounters an opening
 * parenthesis `(`, it calls this function to peek ahead in the string without

infix/src/core/types.c  view on Meta::CPAN

}
/**
 * @internal
 * @struct memo_node_t
 * @brief A memoization node for the deep copy algorithm.
 * @details This temporary structure maps a source `infix_type` address to its
 * newly copied destination address. It is used to prevent re-copying the same
 * object and to correctly reconstruct cyclic type graphs.
 */
typedef struct memo_node_t {
    const infix_type * src;    /**< The original type object's address. */
    infix_type * dest;         /**< The copied type object's address. */
    struct memo_node_t * next; /**< The next node in the memoization list. */
} memo_node_t;
/**
 * @internal
 * @brief Recursively performs a deep copy of a type graph into a destination arena.
 *
 * @details This function is the implementation of the **"Copy"** stage of the data pipeline.
 * It is essential for creating self-contained trampoline objects and for safely
 * managing type lifecycles. It uses memoization to correctly handle cycles and shared

lib/Affix.c  view on Meta::CPAN


    // Rebuild plan
    Newxz(affix->plan, affix->plan_length + 1, Affix_Plan_Step);

    size_t out_param_count = 0;
    OutParamInfo * temp_out_info = safemalloc(sizeof(OutParamInfo) * (affix->num_args > 0 ? affix->num_args : 1));
    size_t current_offset = 0;

    for (size_t i = 0; i < affix->num_args; ++i) {
        // Deep copy types from parse_arena to persistent args_arena
        const infix_type * original_type = _copy_type_graph_to_arena(affix->args_arena, args[i].type);

        // Recalculate offsets (logic duplication from Affix_affix, but necessary)
        size_t alignment, size;
        if (original_type->category == INFIX_TYPE_ARRAY) {
            alignment = _Alignof(void *);
            size = sizeof(void *);
        }
        else {
            alignment = infix_type_get_alignment(original_type);
            size = infix_type_get_size(original_type);
        }
        if (alignment == 0)
            alignment = 1;

        current_offset = (current_offset + alignment - 1) & ~(alignment - 1);
        affix->plan[i].data.c_arg_offset = current_offset;
        current_offset += size;

        affix->plan[i].executor = get_plan_step_executor(original_type);
        affix->plan[i].opcode = get_opcode_for_type(aTHX_ original_type);
        affix->plan[i].data.type = original_type;
        affix->plan[i].data.index = i;

        // Re-detect out params
        if (original_type->category == INFIX_TYPE_POINTER) {
            const infix_type * pointee = original_type->meta.pointer_info.pointee_type;
            const char * pointee_name = infix_type_get_name(pointee);
            if (!pointee_name && pointee->category == INFIX_TYPE_NAMED_REFERENCE)
                pointee_name = pointee->meta.named_reference.name;
            bool is_sv_pointer = pointee_name && (strEQ(pointee_name, "SV") || strEQ(pointee_name, "@SV"));

            if (!is_sv_pointer && pointee->category != INFIX_TYPE_REVERSE_TRAMPOLINE &&
                pointee->category != INFIX_TYPE_VOID) {
                temp_out_info[out_param_count].perl_stack_index = i;
                temp_out_info[out_param_count].pointee_type = pointee;
                temp_out_info[out_param_count].writer = get_out_param_writer(pointee);
                out_param_count++;
            }
        }
        else if (original_type->category == INFIX_TYPE_ARRAY) {
            temp_out_info[out_param_count].perl_stack_index = i;
            temp_out_info[out_param_count].pointee_type = original_type;
            temp_out_info[out_param_count].writer = affix_array_writeback;
            out_param_count++;
        }
    }
    affix->plan[affix->num_args].opcode = OP_DONE;

    // Setup OUT params
    if (out_param_count > 0) {
        affix->out_param_info = safemalloc(sizeof(OutParamInfo) * out_param_count);
        memcpy(affix->out_param_info, temp_out_info, sizeof(OutParamInfo) * out_param_count);

lib/Affix.c  view on Meta::CPAN

    if (status != INFIX_SUCCESS) {
        infix_error_details_t err = infix_get_last_error();
        warn("Failed to parse signature: %s", err.message);
        if (parse_arena)
            infix_arena_destroy(parse_arena);
        XSRETURN_UNDEF;
    }

    // JIT Type substitution (array decay)
    // We create a separate list of types for JIT compilation where Arrays are replaced by Pointers.
    // The original Array types are kept for the marshalling plan.
    infix_type ** jit_arg_types = nullptr;
    if (num_args > 0) {
        jit_arg_types = safemalloc(sizeof(infix_type *) * num_args);
        for (size_t i = 0; i < num_args; ++i) {
            infix_type * t = args[i].type;
            if (t->category == INFIX_TYPE_ARRAY) {
                // Arrays passed as arguments decay to pointers.
                // We create a Pointer[Element] type in the temp arena for JIT creation.
                infix_type * ptr_type = nullptr;
                // FIX: Check return value to satisfy nodiscard warning

lib/Affix.c  view on Meta::CPAN

    affix->plan_length = affix->num_args;
    Newxz(affix->plan, affix->plan_length + 1, Affix_Plan_Step);

    size_t current_offset = 0;
    size_t out_param_count = 0;
    OutParamInfo * temp_out_info = safemalloc(sizeof(OutParamInfo) * (affix->num_args > 0 ? affix->num_args : 1));

    for (size_t i = 0; i < affix->num_args; ++i) {
        // Deep copy from temporary parse_arena to persistent args_arena.
        // We use the ORIGINAL types (args[i].type) so marshalling knows it's an Array.
        const infix_type * original_type = _copy_type_graph_to_arena(affix->args_arena, args[i].type);

        // Calculate offset based on JIT expectation (Array Decay -> Pointer size)
        size_t alignment, size;
        if (original_type->category == INFIX_TYPE_ARRAY) {
            alignment = _Alignof(void *);
            size = sizeof(void *);
        }
        else {
            alignment = infix_type_get_alignment(original_type);
            size = infix_type_get_size(original_type);
        }
        if (alignment == 0)
            alignment = 1;

        current_offset = (current_offset + alignment - 1) & ~(alignment - 1);
        affix->plan[i].data.c_arg_offset = current_offset;
        current_offset += size;

        affix->plan[i].executor = get_plan_step_executor(original_type);
        affix->plan[i].opcode = get_opcode_for_type(aTHX_ original_type);
        affix->plan[i].data.type = original_type;  // Now points to persistent memory
        affix->plan[i].data.index = i;

        if (original_type->category == INFIX_TYPE_POINTER) {
            const infix_type * pointee = original_type->meta.pointer_info.pointee_type;
            const char * pointee_name = infix_type_get_name(pointee);
            if (!pointee_name && pointee->category == INFIX_TYPE_NAMED_REFERENCE)
                pointee_name = pointee->meta.named_reference.name;
            // Skip writeback for Pointer[@SV] to avoid corrupting Perl variables with void return values
            // We assume SV* passed to C is owned by C for the duration and shouldn't be auto-updated
            // (since the SV* itself is the value, not a pointer to a value we want copied back).
            bool is_sv_pointer = pointee_name && (strEQ(pointee_name, "SV") || strEQ(pointee_name, "@SV"));

            if (!is_sv_pointer && pointee->category != INFIX_TYPE_REVERSE_TRAMPOLINE &&
                pointee->category != INFIX_TYPE_VOID) {
                temp_out_info[out_param_count].perl_stack_index = i;
                temp_out_info[out_param_count].pointee_type = pointee;
                temp_out_info[out_param_count].writer = get_out_param_writer(pointee);
                out_param_count++;
            }
        }
        else if (original_type->category == INFIX_TYPE_ARRAY) {
            // Register write-back handler for Arrays
            temp_out_info[out_param_count].perl_stack_index = i;
            temp_out_info[out_param_count].pointee_type = original_type;
            temp_out_info[out_param_count].writer = affix_array_writeback;
            out_param_count++;
        }
    }
    affix->plan[affix->num_args].opcode = OP_DONE;
    affix->total_args_size = current_offset;

    affix->num_out_params = out_param_count;
    if (out_param_count > 0) {
        affix->out_param_info = safemalloc(sizeof(OutParamInfo) * out_param_count);

lib/Affix.c  view on Meta::CPAN

    const char * file = "Unknown";
    int line = 0;
    if (LIKELY(PL_curcop)) {
        file = OutCopFILE(PL_curcop);
        line = CopLINE(PL_curcop);
    }

    /* Call the internal hex-dump utility */
    _DumpHex(aTHX_ ptr, (size_t)length, file, line);

    /* Return the original scalar as a pass-through */
    ST(0) = ST(0);
    XSRETURN(1);
}

XS_INTERNAL(Affix_raw) {
    dVAR;
    dXSARGS;
    if (items != 2)
        croak_xs_usage(cv, "scalar, length_in_bytes");

lib/Affix.pod  view on Meta::CPAN


Binds to a raw memory address (e.g., from C<find_symbol> or pointer arithmetic).

    my $addr = address($some_ptr);
    pin my $val, $addr, Int;

=item B<pin( $var, $existing_pin )>

Clones the binding from an existing pin (copies the address and type).

    pin my $copy, $original_pin;

=back

=head2 C<unpin( $var )>

Removes the magic applied by C<pin>. The variable retains its last value but is no longer linked to C memory.

=head1 TYPE SYSTEM

Map C types to Perl with a rich, built-in vocabulary. Affix signatures are built using helper functions that map

lib/Affix.pod  view on Meta::CPAN


=head3 C<calloc( $count, $size )>

Allocates zero-initialized memory for C<$count> elements of C<$size>. Returns a C<Pointer[Void]> pin.

    my $ptr = calloc( 10, sizeof(Int) );
    my $arr = cast( $ptr, Array[Int, 10] );

=head3 C<realloc( $ptr, $new_size )>

Resizes the memory area pointed to by C<$ptr> to C<$new_size> bytes. The original pin is updated automatically
in-place.

    $ptr = realloc( $ptr, 2048 );

=head3 C<strdup( $string )>

Allocates managed memory and copies the Perl string (along with a C<NULL> terminator) into it. Returns a managed
C<Pointer[Char]> pin.

    my $str_ptr = strdup("Hello C!");

lib/Affix/marshal.c  view on Meta::CPAN

 * suppressing the dereference that is normally required for typed pointers.
 *
 * @param sv The Perl Scalar to inspect.
 * @param ignore_mg A specific magic pointer to skip (prevents self-extraction during assignment).
 * @return The raw C pointer address, or NULL if extraction fails.
 */
void * _extract_pointer_value(pTHX_ SV * sv, MAGIC * ignore_mg) {
    if (!sv)
        return nullptr;

    // Use a secondary pointer for unwrapping to preserve the original SV (the potential object)
    SV * target = sv;
    if (SvROK(sv))
        target = SvRV(sv);

    /* Handle Magic Pins (even inside blessed objects) */
    if (SvMAGICAL(target)) {
        MAGIC * mg = mg_find(target, PERL_MAGIC_ext);
        while (mg) {
            if (is_v2_vtable(mg->mg_virtual) && mg != ignore_mg) {
                Affix_Pin_2_Point_Oh * im = (Affix_Pin_2_Point_Oh *)mg->mg_ptr;

t/007_pointers.t  view on Meta::CPAN


# Cast returns a new pin. We must assign it or use the returned object.
# Also, we keep $mem alive to ensure the memory isn't freed if $int_ptr assumes
# $mem owns it (though cast usually creates unmanaged aliases, so we need $mem to stay alive).
my $int_ptr = cast( malloc( sizeof(Int) ), Array [ Int, 1 ] );

# Test magical 'set' via dereferencing
# $int_ptr is an array ref bound directly to the C memory
$int_ptr->[0] = 42;

# Use the original $mem pointer for reading (verifying they point to the same place)
is read_int_from_void_ptr($int_ptr), 42, 'Magical set via deref wrote to C memory';

# Test cast again
my $long_ptr = cast( $mem, Pointer [LongLong] );
$$long_ptr = 1234567890123;
is $$long_ptr, 1234567890123, 'Magical get after casting to a new type works';

# Test realloc
my $r_ptr = calloc( 2, sizeof Int );

t/007_pointers.t  view on Meta::CPAN

subtest 'Pointer Arithmetic and String Utils' => sub {
    imported_ok qw[ptr_add ptr_diff strdup strnlen is_null];
    subtest 'ptr_add and ptr_diff' => sub {
        my $buf = calloc( 10, sizeof Int );    # 40 bytes
        ok !is_null($buf), 'buffer is not null';
        my $p2 = ptr_add( $buf, 8 );           # width of 2 ints
        is ptr_diff( $p2,  $buf ),  8, 'ptr_diff calculates 8 bytes';
        is ptr_diff( $buf, $p2 ),  -8, 'ptr_diff calculates -8 bytes';
        $$p2 = 999;                            # Write to offset

        # Verify via original array pointer
        my $arr = cast( $buf, Array [ Int, 10 ] );
        is $arr->[2], 999, 'ptr_add moved to index 2 correctly';
    };
    subtest 'strdup and strnlen' => sub {
        my $str = "Hello World";
        my $dup = strdup($str);
        ok !is_null($dup), 'strdup returned non-null';
        is cast( $dup, String ), $str, 'strdup content matches';
        is strnlen( $dup, 5 ),   5,  'strnlen capped at max';
        is strnlen( $dup, 100 ), 11, 'strnlen found true length';

t/007_pointers.t  view on Meta::CPAN

    # Correct cleanup: Use the allocator that created it.
    c_free($string);
    pass('freed via c_free');
};
subtest 'deep pointers' => sub {

    # Deep Indirection (***int)
    isa_ok my $set_deep = wrap( $lib_path, 'set_int_deep', [ Pointer [ Pointer [ Pointer [Int] ] ], Int ] => Void ), ['Affix'];

    # Manually construct the pointer chain with correct types
    # Keep original 'malloc' pointers alive (managed) while using 'cast' aliases
    # Layer 1: The int value (int*)
    my $p_mem = malloc(8);
    my $p_val = Affix::cast( $p_mem, Pointer [Int] );

    # Assigning directly ($p_val = 0) would overwrite the magic scalar with a normal SV*
    $$p_val = 0;

    # Layer 2: Pointer to Layer 1 (int**)
    my $pp_mem = malloc(8);
    my $pp_val = Affix::cast( $pp_mem, Pointer [ Pointer [Int] ] );

t/032_recursive_liveness.t  view on Meta::CPAN

    my $ptr      = malloc( sizeof($Point) * 2 );
    my $live_arr = cast( $ptr, Array [ $Point, 2 ] );

    # Set values
    $live_arr->[0]{x} = 10;
    $live_arr->[0]{y} = 20;

    # Accessing $live_arr->[0] returns an Affix::Live blessed hash
    my $p0 = $live_arr->[0];
    $p0->{x} = 30;
    is $live_arr->[0]{x}, 30, 'Changes to live element reflect in original memory';
};
subtest 'Recursive Liveness: Live struct with Array' => sub {

    # Using typedef to ensure member names are preserved in the infix registry
    typedef ListStruct => Struct [ items => Array [ Int, 3 ] ];
    my $ptr         = malloc( sizeof( ListStruct() ) );
    my $live_struct = cast( $ptr, ListStruct() );

    # Accessing $live_struct->{items} returns an Affix::Pointer object
    my $items = $live_struct->{items};

t/035_magic_struct.t  view on Meta::CPAN

use v5.40;
use Test2::V0;
use blib;
use Affix qw[:all];

# Define our structure
typedef Point => Struct [ x => Int, y => Int ];
typedef Transform => Struct [ origin => Point(), scale => Float ];
#
subtest 'Direct memory write-back' => sub {

    # Allocate raw memory
    my $ptr = Affix::malloc( sizeof( Point() ) );

    # Initialize memory to zeros manually
    memset( $ptr, 0, sizeof( Point() ) );

    # BIND the hash to the pointer (The new system)

t/035_magic_struct.t  view on Meta::CPAN

    is $raw_x, 123, 'C memory for x updated immediately';
    is $raw_y, 456, 'C memory for y updated immediately';
};
subtest 'Reference counting and persistence' => sub {
    my $sub_hash;
    {
        my $root_ptr = Affix::malloc( sizeof( Transform() ) );
        my $tx       = cast $root_ptr, Transform();

        # Grab a reference to a nested struct
        $sub_hash = $tx->{origin};
    }

    # Attempt to write to the sub-hash after parent is technically "gone"
    $sub_hash->{y} = 77;

    # If we are here and haven't crashed, success.
    # We can verify by re-pinning a pointer to see if the value is there.
    is $sub_hash->{y}, 77, 'Sub-struct remains valid and writable after parent scope ends';
};
subtest 'Magical Array Indexing (Primitives)' => sub {

t/083_pin_conventions.t  view on Meta::CPAN

subtest 'pin() 4-arg: pin array type' => sub {
    my $pinned_buf;
    ok pin( $pinned_buf, $lib, 'global_counter', Array [ Int, 1 ] ), 'Pin as Array[Int,1]';
    ok is_pin($pinned_buf),                                          'Result is a pin';
    ok unpin($pinned_buf),                                           'Unpinned array pin';
};
#
subtest 'pin() 2-arg: clone from existing pin' => sub {

    # First create a pinned scalar via 4-arg pin
    my $original;
    ok pin( $original, $lib, 'global_counter', Int() ), '4-arg pin for source';
    $original = 42;
    is $get->(), 42, 'Set global to 42 via original pin';

    # Clone via 2-arg pin
    my $cloned;
    ok pin( $cloned, $original ), '2-arg pin cloned from original';
    is $cloned, 42, 'Cloned pin reads same value';

    # Write through clone, read through original
    $cloned = 77;
    is $original, 77, 'Original sees writes through clone';

    # Write through original, read through clone
    $original = 88;
    is $cloned, 88, 'Clone sees writes through original';

    # Both update C
    is $get->(), 88, 'C global updated through either pin';
    ok unpin($cloned),   'Unpinned clone';
    ok unpin($original), 'Unpinned original';
};
#
subtest 'unpin() returns false for non-pinned scalar' => sub {
    ok !unpin(42),      'unpin returns false for plain integer';
    ok !unpin("hello"), 'unpin returns false for string';
};
#
done_testing;

t/095_union_sync.t  view on Meta::CPAN

use Test2::V0 -no_srand => 1;
use v5.36;

# Regression test for the segfault syncing a union whose members overlap a
# String/pointer field with float data. The rope.pl SDL3 demo crashed when an
# SDL_Event (a union) had `motion.x/y` written and was then passed to
# SDL_PushEvent(): the argument sync (lazy_agg_set) read back *every* union
# member, and reading the inactive `drop.file` (Pointer[String]) dereferenced
# the motion floats (0x42c8...) as a C string pointer -> SIGSEGV.
#
# Members that are still bound to their original C slot are now skipped during
# the deep write (C memory already reflects them), so no garbage is read.
my $lib = compile_ok(<<'');
#include "std.h"
// ext: .c
int poke(void * p) { return 1; }

typedef 'Eventish' => Union [ motion => Struct [ x => Float, y => Float ], drop => Struct [ file => Pointer [String] ] ];
my $buf = Affix::malloc(64);
my $ev  = Affix::cast( $buf, Eventish() );
$ev->{motion}->{x} = 100;

t/999_marshaller_2.t  view on Meta::CPAN

    $mgr->{tasks}[0]{name} = "FFI Core";
    $mgr->{tasks}[1]{id}   = 102;
    $mgr->{tasks}[1]{name} = "Marshal v2";

    # TEST DEEP ACCESS: $comp -> manager (ptr) -> tasks (array) -> name (string)
    is $comp->{manager}{name},         "Alice", 'Deep Read: manager->name';
    is $comp->{manager}{tasks}[0]{id}, 101,     'Deep Read: manager->tasks[0]->id';

    # TEST DEEP WRITE via the pointer member
    $comp->{manager}{tasks}[0]{id} = 999;
    is $mgr->{tasks}[0]{id}, 999, 'Deep Write confirmed in original memory';

    # Reset for verification function
    $comp->{manager}{tasks}[0]{id} = 101;

    # Pass to C
    ok verify_hierarchy($comp), 'FFI: verify_hierarchy($comp) - deep validation passed';
};
subtest 'smart & safety' => sub {

    # Types were defined in previous subtests



( run in 1.693 second using v1.01-cache-2.11-cpan-5c0b1e786e0 )