Interface struct ‣ Allocator ᴾᴴᴾ - chung-leong/zigar GitHub Wiki

JavaScript | PHP


The std.mem.Allocator struct provides an interface to a memory allocator. It is used by Zig code to allocate and free memory. It can be also be used in PHP to allocate memory that's meant for Zig code.

PHP-to-Zig function calls

Zigar automatically provides an allocator to functions that expects one. This allocator obtains memory from the Zend engine.

const std = @import("std");

pub fn dupe(allocator: std.mem.Allocator, s: []const u8) ![]const u8 {
    return try allocator.dupe(s);
}
<?php

$m = zigar_use(__DIR__ . '/allocator-example-1.zig');
$result = $m->dupe('Hello world');
echo "$result\n";
Hello world

In the code above, dupe() has only one required argument on the PHP side: s: []const u8. It accepts an optional named argument, allocator, which may point to an alternate allocator:

const std = @import("std");

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
pub const allocator = gpa.allocator();

pub fn dupe(a: std.mem.Allocator, s: []const u8) ![]const u8 {
    return try a.dupe(u8, s);
}

pub var text_ptr: ?[]const u8 = null;

pub fn print() void {
    std.debug.print("text = {?s}\n", .{text_ptr});
}
<?php

$m = zigar_use(__DIR__ . '/allocator-example-2.zig');

try {
    $m->text_ptr = $m->dupe('Hello world');
} catch (Exception $e) {
    // assignment to pointer will fail because pointers in Zig memory
    // cannot point to PHP memory
    echo "$e\n";
}
$m->print();
$m->text_ptr = $m->dupe('Hello world', allocator: $m->allocator);
$m->print();
Exception: pointers in Zig memory cannot point to garbage-collected object (zig) in /home/rwiggum/examples/allocator-example-2.php:6
Stack trace:
#0 {main}
text = null
text = Hello world

The first call to dupe() returns an object in PHP memory. Our attempt to assign it to text_ptr failed as a result, since the memory is garbage-collected. The second call to dupe() on the other hand returns memory allocated from a GeneralPurposeAllocator, which is acceptable as a pointer target.

Zig-to-PHP function calls

When an allocator is passed to a PHP function, it'll be used to allocate memory for the return value:

const std = @import("std");

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

pub fn call(cb: *const fn (std.mem.Allocator) []const u8) void {
    const str = cb(allocator);
    std.debug.print("string = {s}\n", .{str});
    allocator.free(str);
}
<?php

$m = zigar_use(__DIR__ . '/allocator-example-3.zig');

$m->call(function() {
    return 'Hello world';
});
string = Hello world

Here, the callback function is expected to return a []const u8. The actual function returns a string. And since a PHP string not a proper pointer target, Zigar will automatically create a new slice of u8 with 11 elements and initialize it with the string. Memory will be allocated from the allocator received from the Zig side.

Zigar's auto-vivification mechanism only applies to values that are not backed by an ArrayBuffer. A return value of ArrayBuffer or Uint8Array would trigger auto-casting instead. Consider the following:

<?php

$m = zigar_use(__DIR__ . '/allocator-example-3.zig');

$m->call(function() {
    $string = 'Hello';
    $array = new Uint8Array(strlen($string));
    for ($i = 0; $i < strlen($string); $i++) {
        $array = ord($string[$i]);
    }
    return $array;
});
PHP Warning:  unable to execute callback: pointers in Zig memory cannot point to garbage-collected object (zig) in /home/rwiggum/examples/allocator-example-3b.php on line 12

In this case no memory is allocated for the return value, since Zigar believes that you want to point to the memory in the Uint8Array. And as that's PHP memory, the operation fails and a panic in Zig ensues.

A manual request to duplicate the memory is necessary here:

<?php

$m = zigar_use(__DIR__ . '/allocator-example-3.zig');

$m->call(function($allocator) {
    $string = 'Hello';
    $array = new Uint8Array(strlen($string));
    for ($i = 0; $i < strlen($string); $i++) {
        $array[$i] = ord($string[$i]);
    }
    return $allocator->dupe($array);
});
string = Hello

A better approach is to use Zig memory from the allocator in the first place:

<?php

$m = zigar_use(__DIR__ . '/allocator-example-3.zig');

$m->call(function($allocator) {
    $string = 'Hello';
    $buffer = $allocator->alloc(5);
    $array = new Uint8Array($buffer);
    for ($i = 0; $i < strlen($string); $i++) {
        $array[$i] = ord($string[$i]);
    }
    return $array;
});

alloc() and dupe() are methods in Allocator's special PHP interface. They work in manners analogous to the same functions in Zig.

When a struct is returned by a function, auto-vivification will occurs for its fields:

const std = @import("std");

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

pub const Avenger = struct {
    real_name: []const u8,
    superhero_name: []const u8,
    age: u32,
};

pub fn call(cb: *const fn (std.mem.Allocator) Avenger) void {
    const avenger = cb(allocator);
    std.debug.print("Real name: {s}\n", .{avenger.real_name});
    std.debug.print("Superhero name: {s}\n", .{avenger.superhero_name});
    std.debug.print("Age: {d}\n", .{avenger.age});
    allocator.free(avenger.real_name);
    allocator.free(avenger.superhero_name);
}
<?php

$m = zigar_use(__DIR__ . '/allocator-example-4.zig');

$m->call(function() {
    return [
        'real_name' => 'Tony Stark',
        'superhero_name' => 'Ironman',
        'age' => 53,
    ];
});
Real name: Tony Stark
Superhero name: Ironman
Age: 53

In the above code, memory is allocated for real_name and superhero_name because they're pointers. If the callback function returns *const Avenger instead, memory would be allocated for the struct itself:

const std = @import("std");

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

pub const Avenger = struct {
    real_name: []const u8,
    superhero_name: []const u8,
    age: u32,
};

pub fn call(cb: *const fn (std.mem.Allocator) *const Avenger) void {
    const avenger = cb(allocator);
    std.debug.print("Real name: {s}\n", .{avenger.real_name});
    std.debug.print("Superhero name: {s}\n", .{avenger.superhero_name});
    std.debug.print("Age: {d}\n", .{avenger.age});
    allocator.free(avenger.real_name);
    allocator.free(avenger.superhero_name);
    allocator.destroy(avenger);
}
<?php

$m = zigar_use(__DIR__ . '/allocator-example-5.zig');

$m->call(function() {
    return [
        'real_name' => 'Tony Stark',
        'superhero_name' => 'Ironman',
        'age' => 53,
    ];
});
Real name: Tony Stark
Superhero name: Ironman
Age: 53

The following code performs the same action as above more explicitly:

<?php

$m = zigar_use(__DIR__ . '/allocator-example-5.zig');

$m->call(function($allocator) use($m) {
    return new $m->Avenger([
        'real_name' => 'Tony Stark',
        'superhero_name' => 'Ironman',
        'age' => 53,
    ], allocator: $allocator);
});

NOTE: The examples above all leak memory, as calls to zigar.function.release() were omitted for brevity sake. They are not meant to represent realistic usage scenarios. Passing a PHP function to the Zig side and calling it immediately is almost never useful.


Interface structs | Allocator (PHP interface)