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.
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.
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.