View Source Resources
Resources are datatypes which are managed by the BEAM reference-counted garbage collector. If you are passing data between function calls, generally it is best practice to pass them as a resource instead of as a pointer or a global variable.
For documentation of how resources work in general, see the erlang documentation on the topic. Note that the conveniences in beam.Resource generic type exist to make operations type-safe.
Resources are references
In managed language environments resources are passed as
reference/0
tokens.
Under the hood the BEAM allocates a chunk of memory on resource creation, and this memory is cleared when the garbage collector is triggered. The BEAM also allows you to add in trigger hooks for when this event happens, so custom cleanup can be tied into these GC events.
Passing references between modules
This is not currently supported by Zigler, but support is planned.
Declaring a resource
In order to use a resource, you must do at a minimum three things:
- declare the resource in your
use Zig
directive, under the:resource
key. This is a list of atoms, which match the name of the resource type. - declare the wrapped type. This is the type of the data that is placed in the memory space of the resource.
- use
beam.Resource
to declare the resource type. note that this resource type must bepub
.
defmodule ResourceTest do
use ExUnit.Case, async: true
use Zig,
otp_app: :zigler,
resources: [
:StructResource,
:PointerResource
]
~Z"""
const beam = @import("beam");
const root = @import("root");
const MyStruct = struct {
payload: u64
};
pub const StructResource = beam.Resource(MyStruct, root, .{});
"""
Using resources in functions
~Z"""
pub fn create_resource_term(number: u64) !beam.term {
const res = try StructResource.create(.{.payload = number}, .{});
return beam.make(res, .{});
}
pub fn retrieve_resource_term(term: beam.term) !u64 {
const res = try beam.get(StructResource, term, .{});
return res.unpack().payload;
}
"""
test "lifecyle operations through terms" do
resource = create_resource_term(47)
assert is_reference(resource)
assert 47 = retrieve_resource_term(resource)
end
Resources can be marshalled into and out of beam.term
values using
beam.make
and beam.get
functions as with any other types.
In order to convert between the resource type and the wrapped type, you'll need to use the create
and unpack
functions. Note that create
is failable since under the hood it uses the BEAM
resource allocator.
Direct marshalling
It's possible to directly return resources from a nif function and also pass them as parameters: The nif marshalling functions will be able to detect these types and assign them correctly.
~Z"""
pub fn create_resource_direct(number: u64) !StructResource {
return StructResource.create(.{.payload = number}, .{});
}
pub fn retrieve_resource_direct(resource: StructResource) u64 {
return resource.unpack().payload;
}
"""
test "direct lifecyle operations" do
resource = create_resource_direct(47)
assert is_reference(resource)
assert 47 = retrieve_resource_direct(resource)
end
test "must be the correct type of reference" do
assert_raise ArgumentError, """
errors were found at the given arguments:
* 1st argument:
expected: reference (for `beam.Resource(MyStruct, @import(\"root\"), .{...})`)
got: `%{payload: 42}`
""", fn ->
retrieve_resource_direct(%{payload: 42})
end
non_resource_ref = make_ref()
message = """
errors were found at the given arguments:
* 1st argument:
expected: reference (for `beam.Resource(MyStruct, @import(\"root\"), .{...})`)
got: `#{inspect non_resource_ref}`
note: the reference passed is not associated with a resource of the correct type
"""
assert_raise ArgumentError, message, fn ->
retrieve_resource_direct(non_resource_ref)
end
end
no coercion
You can't pass a term of the same type as the wrapped type and use it within the function.
Wrapping pointers and cleanup
In many cases you won't want to move large data structures into or out of the resource-allocated memory space; this incurs a data copy cost. In that case, you might want to store a pointer in the resource memory space.
In order to do properly clean up after this, you'll need to write a callback function and store it
in a struct namespace that gets associated with the resource type in using the beam.Resource
function .Callbacks
option.
Note that the destructor can also be used in cases where other resources need to be cleaned up, for example file descriptors.
The following functions are supported in the Callbacks, and are all optional.
dtor
: called when the GC collects thestop
: called on stop on behalf ofe.enif_select
down
: called on resource down, on behalf ofe.enif_monitor_process
dyncall
: called on dynamic resource call, on behalf ofenif_dynamic_resource_call
~Z"""
pub const PointerResource = beam.Resource(*MyStruct, root, .{.Callbacks = PointerResourceCallbacks});
pub const PointerResourceCallbacks = struct {
pub fn dtor(s: **MyStruct) void {
beam.allocator.destroy(s.*);
}
};
pub fn create_pointer_resource(number: u64) !PointerResource {
const new_struct = try beam.allocator.create(MyStruct);
new_struct.payload = number;
return PointerResource.create(new_struct, .{});
}
pub fn retrieve_pointer_resource(resource: PointerResource) u64 {
return resource.unpack().*.payload;
}
"""
test "pointer-based lifecyle operations" do
resource = create_pointer_resource(47)
assert is_reference(resource)
assert 47 = retrieve_pointer_resource(resource)
end
pointer allocation strategy
It is strongly recommended to use
beam.allocator
for your pointer payload allocators, asbeam.allocator
is undefined in the callback context.alternatively, if you do use your own managed allocator, you can pack a pointer to the allocator into your datastructure and use this pointer to clean up.
Release and Keep
Resources are associated with release
and keep
functionality. These increment and decrement the
reference count on the resource which allows a nif to prevent the GC from destroying the memory and
calling the destructor. Normally the nif management functions select default release
and keep
settings so that the functions do the most expected outcome - nif functions keep resources while
running and then release them when they're finished.
release on creation
A Struct resource is normally released on creation. This can be disabled by passing
.release = false
into the options parameter ofbeam.Resource(...).create(...)
keep on get
By default,
beam.get
will keep the resource when the internal term retrieved. This can be disabled by passing.keep = false
into the options parameter ofbeam.get
get without keeping
for wrapped datatypes that require a cleanup step (e.g. pointers) it is not recommended to get without keeping, as there could be a race condition where dereferencing the pointer occurs after another nif running in a different OS thread has performed a cleanup against the same resource.
release when function argument
If a function is passed a
beam.Resource(...)
type, it will release it at the end of the call. This can be disabled by setting:noclean
flag in the function argument options. (see nif options)
These functions are provided in the resource type as beam.Resource(...).release(...)
and
beam.Resource(...).keep(...)
functions, respectively:
~Z"""
pub fn release(resource: StructResource) void {
resource.release();
}
pub fn keep(resource: StructResource) void {
resource.keep();
}
"""