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
}