Using Resources
Resources are C ABI objects which you declare should be managed by the BEAM reference-counted garbage collector. If you are passing data between function calls, you should pass the information using a reference.
Note: this API is new and may be subject to change if it's found to be inefficient or difficult.
Basics: Definition
A resource is declared by using the /// resource: <name> definition
directive.
The name
should match a zig type declared in the next line.
Examples
In the following example, the resource my_resource
is bound to the builtin
i64
type. This would allow you to store a mutable 64-bit integer and pass
its pointer between function calls in the BEAM. NOTE: although it's mutable it
is NOT threadsafe.
/// resource: my_resource definition
const my_resource = i64;
In the next example, resource name_resource
is bound to a custom struct type.
This is a more typical situation, where structured data, possibly requiring
internal memory allocation, needs to be passed between function calls.
/// resource: name_resource definition
const name_resource = struct {
first_name: []u8,
last_name: []u8,
};
Basics: Creation
In order to create a resource, a __resource__
struct is autogenerated which
makes manipulating BEAM resources much simpler. When returned from the nif, the
resource will appear as a reference/0
term. As this term is passed around,
potentially between processes, it will only be garbage collected when all
references have been to the term have been lost (more on that in
cleanup).
Note that you will only have access to the __resource__
struct in the context
of code associated with your module in Zigler.sigil_Z/2
segments. Also note
that resource references must remain local, the content cannot be automatically
serialized and deserialized and transferred to another node.
Example
/// nif: create_name/2
fn create_name(env: beam.env, src_first:[]u8, src_last: []u8) beam.term {
var first = beam.allocator.alloc(u8, src_first.len)
catch return beam.raise_enomem(env);
var last = beam.allocator.alloc(u8, src_last.len) catch {
beam.allocator.free(first);
return beam.raise_enomem(env);
}
std.mem.copy(u8, first, src_first);
std.mem.copy(u8, last, src_last);
return __resource__.create(name_resource, env, {
.first_name = first;
.last_name = last;
})
catch beam.raise_function_clause_error(env);
}
Basics: Fetching and Updating
The __resource__
struct has update
and fetch
functions which allow you to
retrieve or modify the contents in the struct. Note that although you can
potentially access these data from different processes, it is not threadsafe by
default without implementing mutexes around the processes.
/// nif: update_last_name/2
fn update_last_name(env: beam.env, resource: beam.term, src_last: []u8) void {
var new_last_name = beam.allocator.alloc(u8, src_last.len)
catch return beam.raise_enomem(env);
var full_name = __resource__.fetch(name_resource, env, value)
catch return beam.raise_function_clause_error(env);
// destroy the old last name and replace it with the new.
beam.allocator.free(full_name.last_name);
full_name.last_name = new_last_name;
return __resource__.update(name_resource, env, resource, full_name)
catch return beam.raise_function_clause_error(env);
}
Basics: Cleanup
Most resources should define a cleanup function. This function will be
invoked when the last reference to that resource has been garbage collected
by the virtual machine. The cleanup function is declared by using the
/// resource: <name> definition
, this should be followed by a void
function taking a pointer to the resource. If there are any internal data
that need freeing, you should do it here.
Note: The memory for the resource itself will be cleaned up by Zigler, so don't free that.
/// resource: name_resource cleanup
fn name_resource_cleanup(env: beam.env, rsrc: *name_resource) void {
beam.allocator.free(name_resource.first_name);
beam.allocator.free(name_resource.last_name);
# don't clean up the name_resource pointer
}