Allocator - chung-leong/zigar GitHub Wiki

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 JavaScript to allocate memory that's meant for Zig code.

JavaScript-to-Zig function calls

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

const std = @import("std");

pub fn dupe(allocator: std.mem.Allocator, s: []const u8) ![]const u8 {
    return try allocator.dupe(s);
}
import { dupe } from './allocator-example-1.zig';

console.log(dupe('Hello world').string);
Hello world

In the code above, dupe() has only one required argument on the JavaScript side: s: []const u8. It accepts a second argument, options, which may contain 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});
}
import { default as mod, allocator, dupe, print } from './allocator-example-2.zig';

try {
    mod.text_ptr = dupe('Hello world');
} catch (err) {
    // assignment to pointer will fail because pointers in Zig memory
    // cannot point to JavaScript memory
}
print();
mod.text_ptr = dupe('Hello world', { allocator });
print();
text = null
text = Hello world

The first call to dupe() returns an object in JavaScript memory. Our attempt to assign it to text_ptr failed as a result, since the memory can be 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-JavaScript function calls

When an allocator is passed to a JavaScript 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);
}
import { call } from './allocator-example-3.zig';

call(() => '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 JavaScript 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 DataView or Uint8Array would trigger auto-casting instead. Consider the following:

import { call } from './allocator-example-3.zig';

call(() => {
    const dv = new DataView(new ArrayBuffer(5));
    for (const [ index, c ] of [ ...'Hello'].entries()) {
        dv.setUint8(index, c.charCodeAt(0));
    }
    return dv;
});
ZigMemoryTargetRequired [TypeError]: Pointers in Zig memory cannot point to garbage-collected object
    at []const u8.set (<anonymous>:1:73821)

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

A manual request to duplicate the memory is necessary here:

import { call } from './allocator-example-3.zig';

call(({ allocator }) => {
    const dv = new DataView(new ArrayBuffer(5));
    for (const [ index, c ] of [ ...'Hello'].entries()) {
        dv.setUint8(index, c.charCodeAt(0));
    }
    return allocator.dupe(dv);
});
string = Hello

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

import { call } from './allocator-example-3.zig';

call(({ allocator }) => {
    const dv = allocator.alloc(5);
    for (const [ index, c ] of [ ...'Hello'].entries()) {
        dv.setUint8(index, c.charCodeAt(0));
    }
    return dv;
});

alloc() and dupe() are methods in Allocator's special JavaScript 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);
}
import { call } from './allocator-example-4.zig';

call(() => ({
  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);
}
import { call } from './allocator-example-5.zig';

call(() => ({
  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:

import { call, Avenger } from './allocator-example-5.zig';

call(({ allocator }) => {
    const fields = {
        real_name: 'Tony Stark',
        superhero_name: 'Ironman',
        age: 53,
    };
    return new Avenger(fields, { 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 JavaScript function to the Zig side and calling it immediately is almost never useful.


Allocator (JavaScript interface)

⚠️ **GitHub.com Fallback** ⚠️