├── test ├── test_helper.exs └── kinda_test.exs ├── kinda_example ├── native │ ├── zig-src │ │ ├── .gitignore │ │ ├── prelude.zig │ │ └── main.zig │ └── c-src │ │ ├── include │ │ ├── wrapper.h │ │ └── KindaExample.h │ │ ├── lib │ │ └── KindaExample.cpp │ │ └── CMakeLists.txt ├── test │ ├── test_helper.exs │ └── kinda_example_test.exs ├── config │ ├── config.exs │ ├── dev.exs │ ├── prod.exs │ └── test.exs ├── README.md ├── .formatter.exs ├── build.zig.zon ├── lib │ ├── kinda_example_code_gen.ex │ ├── kinda_example_native.ex │ └── kinda_example_nif.ex ├── Makefile ├── .gitignore ├── mix.exs └── build.zig ├── lib ├── prebuilt_meta.ex ├── forwarder.ex ├── kinda │ └── error │ │ └── nif_call.ex ├── kinda.ex ├── resource_kind.ex ├── codegen │ ├── nif_decl.ex │ └── kind_decl.ex ├── zig_precompiler.ex └── kinda_codegen.ex ├── .formatter.exs ├── scripts └── gdb.sh ├── .gitignore ├── mix.exs ├── src ├── result.zig ├── kinda.zig └── beam.zig ├── mix.lock ├── .github └── workflows │ └── elixir.yml └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /kinda_example/native/zig-src/.gitignore: -------------------------------------------------------------------------------- 1 | *.imp.zig 2 | -------------------------------------------------------------------------------- /kinda_example/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /kinda_example/native/c-src/include/wrapper.h: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /kinda_example/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | import_config "#{config_env()}.exs" 4 | -------------------------------------------------------------------------------- /test/kinda_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KindaTest do 2 | use ExUnit.Case 3 | doctest Kinda 4 | end 5 | -------------------------------------------------------------------------------- /kinda_example/README.md: -------------------------------------------------------------------------------- 1 | # KindaExample 2 | 3 | This is an example app to show how to use Kinda. 4 | -------------------------------------------------------------------------------- /kinda_example/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :kinda, :force_build, kinda_example: true 4 | -------------------------------------------------------------------------------- /kinda_example/config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :kinda, :force_build, kinda_example: true 4 | -------------------------------------------------------------------------------- /kinda_example/config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :kinda, :force_build, kinda_example: true 4 | -------------------------------------------------------------------------------- /kinda_example/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /kinda_example/native/zig-src/prelude.zig: -------------------------------------------------------------------------------- 1 | pub const c = @cImport({ 2 | @cDefine("_NO_CRT_STDIO_INLINE", "1"); 3 | @cInclude("wrapper.h"); 4 | }); 5 | -------------------------------------------------------------------------------- /lib/prebuilt_meta.ex: -------------------------------------------------------------------------------- 1 | defmodule Kinda.Prebuilt.Meta do 2 | defstruct [ 3 | :nifs, 4 | :resource_kinds, 5 | :zig_t_module_map 6 | ] 7 | end 8 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: 4 | ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] ++ 5 | ["kinda_example/{mix,.formatter}.exs", "kinda_example/{config,lib,test}/**/*.{ex,exs}"] 6 | ] 7 | -------------------------------------------------------------------------------- /kinda_example/native/c-src/lib/KindaExample.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #ifdef __cplusplus 3 | extern "C" { 4 | #endif 5 | 6 | int kinda_example_add(int a, int b) { return a + b; } 7 | 8 | #ifdef __cplusplus 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /lib/forwarder.ex: -------------------------------------------------------------------------------- 1 | defmodule Kinda.Forwarder do 2 | @callback forward() :: :ok | :error 3 | @callback check!() :: :ok | :error 4 | @callback array() :: :ok | :error 5 | @callback to_term() :: :ok | :error 6 | @callback opaque_ptr() :: :ok | :error 7 | end 8 | -------------------------------------------------------------------------------- /kinda_example/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .kinda_example, 3 | .version = "0.0.0", 4 | .fingerprint = 0xbd855cbe3871e932, 5 | .dependencies = .{ 6 | .kinda = .{ 7 | .path = "..", 8 | }, 9 | }, 10 | .paths = .{""}, 11 | } 12 | -------------------------------------------------------------------------------- /lib/kinda/error/nif_call.ex: -------------------------------------------------------------------------------- 1 | defmodule Kinda.CallError do 2 | defexception [:message] 3 | 4 | @impl true 5 | def message(t) do 6 | notice = "to see the full stack trace, set KINDA_DUMP_STACK_TRACE=1" 7 | 8 | """ 9 | #{t.message} 10 | #{notice} 11 | """ 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /kinda_example/native/c-src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13.4) 2 | set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "") 3 | project(kinda-example) 4 | 5 | add_library(KindaExample SHARED lib/KindaExample.cpp) 6 | target_include_directories(KindaExample PUBLIC include) 7 | install(TARGETS KindaExample DESTINATION lib) 8 | -------------------------------------------------------------------------------- /lib/kinda.ex: -------------------------------------------------------------------------------- 1 | defmodule Kinda do 2 | @moduledoc """ 3 | Documentation for `Kinda`. 4 | """ 5 | 6 | def unwrap_ref(%{ref: ref}) do 7 | ref 8 | end 9 | 10 | def unwrap_ref(arguments) when is_list(arguments) do 11 | Enum.map(arguments, &unwrap_ref/1) 12 | end 13 | 14 | def unwrap_ref(term) do 15 | term 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /kinda_example/native/c-src/include/KindaExample.h: -------------------------------------------------------------------------------- 1 | #ifndef KINDA_EXAMPLE_NATIVE_C_SRC_INCLUDE_KINDAEXAMPLE_H_ 2 | #define KINDA_EXAMPLE_NATIVE_C_SRC_INCLUDE_KINDAEXAMPLE_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | int kinda_example_add(int a, int b); 9 | 10 | #ifdef __cplusplus 11 | } 12 | #endif 13 | 14 | #endif // KINDA_EXAMPLE_NATIVE_C_SRC_INCLUDE_KINDAEXAMPLE_H_ 15 | -------------------------------------------------------------------------------- /kinda_example/lib/kinda_example_code_gen.ex: -------------------------------------------------------------------------------- 1 | defmodule KindaExample.CodeGen do 2 | @moduledoc false 3 | alias Kinda.CodeGen.{KindDecl} 4 | @behaviour Kinda.CodeGen 5 | @impl true 6 | def kinds() do 7 | [ 8 | %KindDecl{ 9 | module_name: KindaExample.NIF.CInt 10 | }, 11 | %KindDecl{ 12 | module_name: KindaExample.NIF.StrInt 13 | } 14 | ] 15 | end 16 | 17 | @impl true 18 | def nifs() do 19 | [ 20 | kinda_example_add: 2 21 | ] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /scripts/gdb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ROOT_DIR=$(elixir --eval ":code.root_dir() |> IO.puts()") 5 | VERSION=$(elixir --eval ":erlang.system_info(:version) |> IO.puts()") 6 | export BINDIR=$ROOT_DIR/erts-$VERSION/bin 7 | EXE=$BINDIR/erlexec 8 | 9 | echo "ROOT_DIR: $ROOT_DIR" 10 | echo "VERSION: $VERSION" 11 | echo "EXE: $EXE" 12 | 13 | FULL=$(ELIXIR_CLI_DRY_RUN=1 mix "$@") 14 | FULL=${FULL/erl/${EXE}} 15 | echo "FULL: $FULL" 16 | 17 | # Run under GDB with automated backtrace 18 | gdb -ex "run" -ex "bt" -ex "quit" --args $FULL 19 | -------------------------------------------------------------------------------- /kinda_example/lib/kinda_example_native.ex: -------------------------------------------------------------------------------- 1 | defmodule KindaExample.Native do 2 | def check!({:kind, mod, ref}) when is_atom(mod) and is_reference(ref) do 3 | struct!(mod, %{ref: ref}) 4 | end 5 | 6 | def check!({:error, e}), do: raise(e) 7 | def check!(ret), do: ret 8 | 9 | def to_term(%mod{ref: ref}) do 10 | forward(mod, :primitive, [ref]) 11 | end 12 | 13 | def forward(element_kind, kind_func_name, args) do 14 | apply(KindaExample.NIF, Module.concat(element_kind, kind_func_name), args) 15 | |> check!() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/resource_kind.ex: -------------------------------------------------------------------------------- 1 | defmodule Kinda.ResourceKind do 2 | defmacro __using__(opts) do 3 | forward_module = Keyword.fetch!(opts, :forward_module) 4 | fields = Keyword.get(opts, :fields) || [] 5 | gen_spec = Keyword.get(opts, :gen_spec, true) 6 | 7 | spec = 8 | if gen_spec do 9 | quote do 10 | @type t() :: %__MODULE__{} 11 | end 12 | end 13 | 14 | quote do 15 | defstruct [ref: nil] ++ unquote(fields) 16 | 17 | unquote(spec) 18 | 19 | def make(value) do 20 | %__MODULE__{ 21 | ref: unquote(forward_module).forward(__MODULE__, "make", [value]) 22 | } 23 | end 24 | 25 | defoverridable(make: 1) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /kinda_example/Makefile: -------------------------------------------------------------------------------- 1 | CMAKE_BUILD_DIR = ${MIX_APP_PATH}/cmake_build 2 | NATIVE_INSTALL_DIR = ${MIX_APP_PATH}/priv 3 | ZIG_CACHE_DIR = ${MIX_APP_PATH}/zig_cache 4 | .PHONY: all zig_build cmake_build 5 | 6 | all: zig_build 7 | 8 | zig_build: cmake_build 9 | zig build --cache-dir ${ZIG_CACHE_DIR} \ 10 | --prefix ${NATIVE_INSTALL_DIR} \ 11 | --search-prefix native/c-src \ 12 | --search-prefix ${NATIVE_INSTALL_DIR} \ 13 | --search-prefix ${ERTS_INCLUDE_DIR}/.. \ 14 | -freference-trace 15 | 16 | cmake_build: 17 | cmake -G Ninja -S native/c-src -B ${CMAKE_BUILD_DIR} -DCMAKE_INSTALL_PREFIX=${NATIVE_INSTALL_DIR} 18 | cmake --build ${CMAKE_BUILD_DIR} --target install 19 | 20 | clean: 21 | rm -rf ${CMAKE_BUILD_DIR} 22 | rm -rf ${NATIVE_INSTALL_DIR} 23 | -------------------------------------------------------------------------------- /kinda_example/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | kinda_example-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | kinda-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | .elixir_ls 29 | /kinda_example/mix.lock 30 | zig-cache 31 | .zig-cache 32 | -------------------------------------------------------------------------------- /kinda_example/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule KindaExample.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :kinda_example, 7 | version: "0.1.0-dev", 8 | elixir: "~> 1.9", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | compilers: [:elixir_make] ++ Mix.compilers() 12 | ] ++ 13 | [ 14 | make_precompiler: {:nif, Kinda.Precompiler}, 15 | make_precompiler_url: "http://127.0.0.1:8000/@{artefact_filename}" 16 | ] 17 | end 18 | 19 | # Run "mix help compile.app" to learn about applications. 20 | def application do 21 | [ 22 | extra_applications: [:logger] 23 | ] 24 | end 25 | 26 | # Run "mix help deps" to learn about dependencies. 27 | defp deps do 28 | [ 29 | {:kinda, path: ".."}, 30 | {:elixir_make, "~> 0.4", runtime: false} 31 | # {:dep_from_hexpm, "~> 0.3.0"}, 32 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 33 | ] 34 | end 35 | 36 | require Logger 37 | end 38 | -------------------------------------------------------------------------------- /lib/codegen/nif_decl.ex: -------------------------------------------------------------------------------- 1 | defmodule Kinda.CodeGen.NIFDecl do 2 | alias Kinda.CodeGen.KindDecl 3 | @type dirty() :: :io | :cpu | false 4 | @type t() :: %__MODULE__{ 5 | wrapper_name: nil | String.t(), 6 | nif_name: nil | String.t(), 7 | params: [String.t()] | integer() 8 | } 9 | defstruct nif_name: nil, arity: 0, wrapper_name: nil, params: nil 10 | 11 | # TODO: make this extensible 12 | def from_resource_kind(%KindDecl{module_name: module_name, kind_functions: kind_functions}) do 13 | for {f, a} <- 14 | [ 15 | ptr: 1, 16 | ptr_to_opaque: 1, 17 | opaque_ptr: 1, 18 | array: 1, 19 | mut_array: 1, 20 | primitive: 1, 21 | make: 1, 22 | dump: 1, 23 | make_from_opaque_ptr: 2, 24 | array_as_opaque: 1 25 | ] ++ kind_functions do 26 | %__MODULE__{ 27 | nif_name: Module.concat(module_name, f), 28 | wrapper_name: Module.concat(module_name, f), 29 | params: a 30 | } 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Kinda.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :kinda, 7 | version: "0.10.9-dev", 8 | elixir: "~> 1.9", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | description: description(), 12 | package: package() 13 | ] 14 | end 15 | 16 | # Run "mix help compile.app" to learn about applications. 17 | def application do 18 | [ 19 | extra_applications: [:logger] 20 | ] 21 | end 22 | 23 | # Run "mix help deps" to learn about dependencies. 24 | defp deps do 25 | [ 26 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 27 | {:elixir_make, "~> 0.4", runtime: false} 28 | ] 29 | end 30 | 31 | defp description() do 32 | "Bind a C library to BEAM with Zig." 33 | end 34 | 35 | defp package() do 36 | [ 37 | licenses: ["Apache-2.0"], 38 | links: %{"GitHub" => "https://github.com/beaver-project/kinda"}, 39 | files: ~w{ 40 | lib .formatter.exs mix.exs README* 41 | src/*.zig build.zig 42 | } 43 | ] 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /kinda_example/lib/kinda_example_nif.ex: -------------------------------------------------------------------------------- 1 | defmodule KindaExample.NIF do 2 | @nifs use Kinda.CodeGen, 3 | with: KindaExample.CodeGen, 4 | root: __MODULE__, 5 | forward: KindaExample.Native 6 | defmodule CInt do 7 | use Kinda.ResourceKind, forward_module: KindaExample.Native 8 | end 9 | 10 | for path <- 11 | Path.wildcard("native/c-src/**/*.h") ++ 12 | Path.wildcard("native/c-src/**/*.cpp") ++ 13 | Path.wildcard("../src/**/*.zig") ++ 14 | ["../build.zig", "../build.example.zig"] do 15 | @external_resource path 16 | end 17 | 18 | defmodule StrInt do 19 | use Kinda.ResourceKind, forward_module: KindaExample.Native 20 | end 21 | 22 | @on_load :load_nif 23 | 24 | def load_nif do 25 | nif_file = ~c"#{:code.priv_dir(:kinda_example)}/lib/libKindaExampleNIF" 26 | 27 | if File.exists?(dylib = "#{nif_file}.dylib") do 28 | File.ln_s(dylib, "#{nif_file}.so") 29 | end 30 | 31 | case :erlang.load_nif(nif_file, 0) do 32 | :ok -> :ok 33 | {:error, {:reload, _}} -> :ok 34 | {:error, reason} -> IO.puts("Failed to load nif: #{inspect(reason)}") 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /kinda_example/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const os = builtin.os.tag; 4 | 5 | pub fn build(b: *std.Build) void { 6 | // Standard release options allow the person running `zig build` to select 7 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. 8 | const target = b.standardTargetOptions(.{}); 9 | const optimize = b.standardOptimizeOption(.{}); 10 | 11 | const lib = b.addLibrary(.{ 12 | .name = "KindaExampleNIF", 13 | .linkage = .dynamic, 14 | .root_module = b.createModule(.{ 15 | .root_source_file = b.path("native/zig-src/main.zig"), 16 | .target = target, 17 | .optimize = optimize, 18 | }), 19 | }); 20 | const kinda = b.dependencyFromBuildZig(@import("kinda"), .{}); 21 | lib.root_module.addImport("kinda", kinda.module("kinda")); 22 | if (os.isDarwin()) { 23 | lib.root_module.addRPathSpecial("@loader_path"); 24 | } else { 25 | lib.root_module.addRPathSpecial("$ORIGIN"); 26 | } 27 | lib.linkSystemLibrary("KindaExample"); 28 | lib.linker_allow_shlib_undefined = true; 29 | 30 | b.installArtifact(lib); 31 | } 32 | -------------------------------------------------------------------------------- /src/result.zig: -------------------------------------------------------------------------------- 1 | const beam = @import("beam.zig"); 2 | const e = @import("kinda.zig").erl_nif; 3 | const std = @import("std"); 4 | 5 | pub fn is_stack_trace_enabled() bool { 6 | var value: [256]u8 = undefined; 7 | var value_size: usize = value.len; 8 | return e.enif_getenv("KINDA_DUMP_STACK_TRACE", &value[0], &value_size) == 0; 9 | } 10 | 11 | pub fn nif_with_flags(comptime name: [*c]const u8, comptime arity: usize, comptime f: anytype, comptime flags: u32) type { 12 | const ns = "Elixir."; 13 | return struct { 14 | fn exported(env: beam.env, n: c_int, args: [*c]const beam.term) callconv(.c) beam.term { 15 | return f(env, n, args) catch |err| { 16 | if (is_stack_trace_enabled()) { 17 | std.debug.dumpStackTrace(@errorReturnTrace().?.*); 18 | } 19 | return beam.raise_exception(env, ns ++ "Kinda.CallError", err); 20 | }; 21 | } 22 | pub const entry = e.ErlNifFunc{ .name = name, .arity = arity, .fptr = exported, .flags = flags }; 23 | }; 24 | } 25 | 26 | pub fn nif(comptime name: [*c]const u8, comptime arity: usize, comptime f: anytype) type { 27 | return nif_with_flags(name, arity, f, 0); 28 | } 29 | 30 | pub fn wrap(comptime f: anytype) fn (env: beam.env, n: c_int, args: [*c]const beam.term) callconv(.C) beam.term { 31 | return nif("", 0, f).exported; 32 | } 33 | -------------------------------------------------------------------------------- /kinda_example/test/kinda_example_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KindaExampleTest do 2 | use ExUnit.Case 3 | 4 | alias KindaExample.{NIF, Native} 5 | 6 | test "add in c" do 7 | assert 3 == 8 | NIF.kinda_example_add(1, 2) |> Native.to_term() 9 | 10 | assert match?( 11 | %Kinda.CallError{message: "Fail to fetch argument #2"}, 12 | catch_error(NIF.kinda_example_add(1, "2")) 13 | ) 14 | end 15 | 16 | test "custom make" do 17 | assert 100 == 18 | Native.forward(NIF.CInt, :make, [100]) 19 | |> NIF."Elixir.KindaExample.NIF.CInt.primitive"() 20 | 21 | e = catch_error(NIF."Elixir.KindaExample.NIF.StrInt.make"(1)) 22 | assert Exception.message(e) =~ "Function clause error\n" 23 | 24 | err = catch_error(NIF."Elixir.KindaExample.NIF.StrInt.make"(1)) 25 | # only test this on macOS, it will crash on Linux 26 | txt = Exception.message(err) 27 | 28 | assert txt =~ "to see the full stack trace, set KINDA_DUMP_STACK_TRACE=1" 29 | 30 | assert match?(%Kinda.CallError{message: "Function clause error"}, err) 31 | 32 | assert 1 == 33 | NIF."Elixir.KindaExample.NIF.StrInt.make"("1") 34 | |> NIF."Elixir.KindaExample.NIF.CInt.primitive"() 35 | 36 | %NIF.StrInt{ref: ref} = NIF.StrInt.make("1") 37 | assert 1 == ref |> NIF."Elixir.KindaExample.NIF.CInt.primitive"() 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/zig_precompiler.ex: -------------------------------------------------------------------------------- 1 | defmodule Kinda.Precompiler do 2 | @behaviour ElixirMake.Precompiler 3 | 4 | def current_target({:unix, _}) do 5 | # get current target triplet from `:erlang.system_info/1` 6 | system_architecture = to_string(:erlang.system_info(:system_architecture)) 7 | current = String.split(system_architecture, "-", trim: true) 8 | 9 | case length(current) do 10 | 4 -> 11 | {:ok, "#{Enum.at(current, 0)}-#{Enum.at(current, 2)}-#{Enum.at(current, 3)}"} 12 | 13 | 3 -> 14 | case :os.type() do 15 | {:unix, :darwin} -> 16 | # could be something like aarch64-apple-darwin21.0.0 17 | # but we don't really need the last 21.0.0 part 18 | if String.match?(Enum.at(current, 2), ~r/^darwin.*/) do 19 | {:ok, "#{Enum.at(current, 0)}-#{Enum.at(current, 1)}-darwin"} 20 | else 21 | {:ok, system_architecture} 22 | end 23 | 24 | _ -> 25 | {:ok, system_architecture} 26 | end 27 | 28 | _ -> 29 | {:error, "cannot decide current target"} 30 | end 31 | end 32 | 33 | @impl ElixirMake.Precompiler 34 | def current_target do 35 | current_target(:os.type()) 36 | end 37 | 38 | @impl ElixirMake.Precompiler 39 | def all_supported_targets(:fetch) do 40 | ~w( 41 | aarch64-apple-darwin 42 | x86_64-apple-darwin 43 | x86_64-linux-gnu 44 | aarch64-linux-gnu 45 | ) 46 | end 47 | 48 | def all_supported_targets(:compile) do 49 | {:ok, t} = current_target() 50 | [t] 51 | end 52 | 53 | @impl ElixirMake.Precompiler 54 | def build_native(args) do 55 | ElixirMake.Compiler.compile(args) 56 | end 57 | 58 | @impl ElixirMake.Precompiler 59 | def precompile(args, _target) do 60 | build_native(args) 61 | :ok 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /kinda_example/native/zig-src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const kinda = @import("kinda"); 3 | const beam = kinda.beam; 4 | const e = kinda.erl_nif; 5 | const capi = @import("prelude.zig").c; 6 | const root_module = "Elixir.KindaExample.NIF"; 7 | const Kinds = struct { 8 | const CInt = kinda.ResourceKind(c_int, root_module ++ ".CInt"); 9 | const StrInt = kinda.ResourceKind(extern struct { 10 | i: c_int = 0, 11 | fn make(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 12 | var s: beam.binary = try beam.get_binary(env, args[0]); 13 | const integer = try std.fmt.parseInt(i32, s.data[0..s.size], 10); 14 | return CInt.resource.make(env, integer) catch return beam.Error.@"Fail to make resource"; 15 | } 16 | pub const maker = .{ make, 1 }; 17 | }, root_module ++ ".StrInt"); 18 | const All = .{ CInt, StrInt }; 19 | fn open(env: beam.env) void { 20 | inline for (All) |k| { 21 | k.open_all(env); 22 | } 23 | } 24 | }; 25 | 26 | const all_nifs = .{ 27 | kinda.NIFFunc(Kinds.All, capi, "kinda_example_add", .{ .nif_name = "Elixir.KindaExample.NIF.kinda_example_add" }), 28 | } ++ Kinds.CInt.nifs ++ Kinds.StrInt.nifs; 29 | pub export var nifs: [all_nifs.len]e.ErlNifFunc = all_nifs; 30 | 31 | const entry = e.ErlNifEntry{ 32 | .major = 2, 33 | .minor = 16, 34 | .name = root_module, 35 | .num_of_funcs = nifs.len, 36 | .funcs = &(nifs[0]), 37 | .load = nif_load, 38 | .reload = null, // currently unsupported 39 | .upgrade = null, // currently unsupported 40 | .unload = null, // currently unsupported 41 | .vm_variant = "beam.vanilla", 42 | .options = 1, 43 | .sizeof_ErlNifResourceTypeInit = @sizeOf(e.ErlNifResourceTypeInit), 44 | .min_erts = "erts-13.0", 45 | }; 46 | 47 | export fn nif_load(env: beam.env, _: [*c]?*anyopaque, _: beam.term) c_int { 48 | Kinds.open(env); 49 | return 0; 50 | } 51 | 52 | export fn nif_init() *const e.ErlNifEntry { 53 | return &entry; 54 | } 55 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 3 | "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, 4 | "ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"}, 5 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 7 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 8 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 9 | } 10 | -------------------------------------------------------------------------------- /lib/kinda_codegen.ex: -------------------------------------------------------------------------------- 1 | defmodule Kinda.CodeGen do 2 | @moduledoc """ 3 | Behavior for customizing your source code generation. 4 | """ 5 | 6 | alias Kinda.CodeGen.{KindDecl, NIFDecl} 7 | 8 | defmacro __using__(opts) do 9 | quote do 10 | mod = Keyword.fetch!(unquote(opts), :with) 11 | root = Keyword.fetch!(unquote(opts), :root) 12 | forward = Keyword.fetch!(unquote(opts), :forward) 13 | {ast, mf} = Kinda.CodeGen.nif_ast(mod.kinds(), mod.nifs(), root, forward) 14 | ast |> Code.eval_quoted([], __ENV__) 15 | mf 16 | end 17 | end 18 | 19 | @callback kinds() :: [KindDecl.t()] 20 | @callback nifs() :: [{atom(), integer()}] 21 | def kinds(), do: [] 22 | 23 | def nif_ast(kinds, nifs, root_module, forward_module) do 24 | # generate stubs for generated NIFs 25 | extra_kind_nifs = 26 | kinds 27 | |> Enum.map(&NIFDecl.from_resource_kind/1) 28 | |> List.flatten() 29 | 30 | nifs = nifs ++ extra_kind_nifs 31 | 32 | for nif <- nifs do 33 | nif = 34 | case nif do 35 | {wrapper_name, arity} when is_atom(wrapper_name) and is_integer(arity) -> 36 | %NIFDecl{ 37 | wrapper_name: wrapper_name, 38 | nif_name: Module.concat(root_module, wrapper_name), 39 | params: arity 40 | } 41 | 42 | {wrapper_name, params} when is_atom(wrapper_name) and is_list(params) -> 43 | %NIFDecl{ 44 | wrapper_name: wrapper_name, 45 | nif_name: Module.concat(root_module, wrapper_name), 46 | params: params 47 | } 48 | 49 | %NIFDecl{} -> 50 | nif 51 | end 52 | 53 | {args_ast, arity} = 54 | if is_list(nif.params) do 55 | {Enum.map(nif.params, &Macro.var(&1, __MODULE__)), length(nif.params)} 56 | else 57 | {Macro.generate_unique_arguments(nif.params, __MODULE__), nif.params} 58 | end 59 | 60 | %NIFDecl{wrapper_name: wrapper_name, nif_name: nif_name} = nif 61 | 62 | wrapper_name = 63 | if is_bitstring(wrapper_name) do 64 | String.to_atom(wrapper_name) 65 | else 66 | wrapper_name 67 | end 68 | 69 | wrapper_ast = 70 | if nif_name != wrapper_name do 71 | quote do 72 | def unquote(wrapper_name)(unquote_splicing(args_ast)) do 73 | refs = Kinda.unwrap_ref([unquote_splicing(args_ast)]) 74 | ret = apply(__MODULE__, unquote(nif_name), refs) 75 | unquote(forward_module).check!(ret) 76 | end 77 | end 78 | end 79 | 80 | quote do 81 | @doc false 82 | def unquote(nif_name)(unquote_splicing(args_ast)), 83 | do: :erlang.nif_error(:not_loaded) 84 | 85 | unquote(wrapper_ast) 86 | end 87 | |> then(&{&1, {nif_name, arity}}) 88 | end 89 | |> Enum.unzip() 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | pull_request: 5 | branches: ["main"] 6 | 7 | permissions: 8 | contents: read 9 | 10 | concurrency: 11 | group: manx-build-and-test-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | KINDA_PRINT_LINKAGES: 1 16 | 17 | jobs: 18 | build: 19 | name: otp${{matrix.otp}}-ex${{matrix.elixir}} / ${{matrix.runs-on}} 20 | runs-on: ${{matrix.runs-on}} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | runs-on: ["ubuntu-22.04"] 25 | otp: ["24.2", "25.0"] 26 | elixir: ["1.13.0", "1.16.2", "1.18.0"] 27 | exclude: 28 | - otp: "24.2" 29 | elixir: "1.18.0" 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: erlef/setup-beam@v1 33 | with: 34 | otp-version: ${{matrix.otp}} 35 | elixir-version: ${{matrix.elixir}} 36 | - name: Set up Zig 37 | uses: goto-bus-stop/setup-zig@v1 38 | with: 39 | version: 0.15.1 40 | - uses: seanmiddleditch/gha-setup-ninja@master 41 | - name: Restore dependencies cache 42 | uses: actions/cache@v3 43 | with: 44 | path: deps 45 | key: ${{ runner.os }}-mix-kinda-${{ hashFiles('**/mix.lock') }} 46 | restore-keys: ${{ runner.os }}-mix-kinda- 47 | - name: Restore dependencies cache 48 | uses: actions/cache@v3 49 | with: 50 | path: ./kinda_example/deps 51 | key: ${{ runner.os }}-mix-example-${{ hashFiles('**/mix.exs') }} 52 | restore-keys: ${{ runner.os }}-mix-example- 53 | - name: Install dependencies 54 | run: mix deps.get 55 | - name: Check formatting of Elixir 56 | run: mix format --check-formatted 57 | - name: Run tests 58 | run: | 59 | mix test 60 | - name: Install dependencies (example) 61 | working-directory: ./kinda_example 62 | run: mix deps.get 63 | - name: Check formatting of Elixir 64 | working-directory: ./kinda_example 65 | run: mix format --check-formatted 66 | - name: Check formatting of Zig 67 | run: | 68 | zig fmt --check src 69 | zig fmt --check kinda_example/native/zig-src 70 | - name: Install gdb 71 | run: | 72 | sudo apt update 73 | sudo apt install -y gdb 74 | - name: Run tests (example with gdb and stack trace) 75 | working-directory: ./kinda_example 76 | run: | 77 | bash ../scripts/gdb.sh test 78 | env: 79 | KINDA_DUMP_STACK_TRACE: "1" 80 | - name: Run tests (example with gdb) 81 | working-directory: ./kinda_example 82 | run: | 83 | bash ../scripts/gdb.sh test 84 | - name: Run tests (example) 85 | working-directory: ./kinda_example 86 | run: | 87 | mix test --force 88 | - name: Run tests (example with stack trace) 89 | continue-on-error: true 90 | working-directory: ./kinda_example 91 | run: | 92 | mix test --force 93 | env: 94 | KINDA_DUMP_STACK_TRACE: "1" 95 | - name: Precompile 96 | working-directory: ./kinda_example 97 | env: 98 | MIX_ENV: prod 99 | ELIXIR_MAKE_CACHE_DIR: . 100 | run: | 101 | rm -rf _build 102 | mix elixir_make.precompile 103 | - name: Start mock server in the background 104 | working-directory: ./kinda_example 105 | run: | 106 | python3 -m http.server --directory . &> /dev/null & 107 | sleep 3 108 | ps aux 109 | - name: Test precompiled 110 | working-directory: ./kinda_example 111 | run: | 112 | MIX_ENV=prod mix elixir_make.checksum --all --ignore-unavailable --only-local --print 113 | mix clean 114 | mix test --exclude vulkan --exclude todo 115 | -------------------------------------------------------------------------------- /lib/codegen/kind_decl.ex: -------------------------------------------------------------------------------- 1 | defmodule Kinda.CodeGen.KindDecl do 2 | require Logger 3 | 4 | @primitive_types ~w{ 5 | bool 6 | c_int 7 | c_uint 8 | f32 9 | f64 10 | i16 11 | i32 12 | i64 13 | i8 14 | isize 15 | u16 16 | u32 17 | u64 18 | u8 19 | usize 20 | }a 21 | 22 | @opaque_ptr {:optional_type, {:ref, [:*, :anyopaque]}} 23 | @opaque_array {:optional_type, {:ref, [:*, :const, :anyopaque]}} 24 | 25 | def primitive_types do 26 | @primitive_types 27 | end 28 | 29 | @primitive_types_set MapSet.new(@primitive_types) 30 | def is_primitive_type?(t) do 31 | MapSet.member?(@primitive_types_set, t) 32 | end 33 | 34 | def is_opaque_ptr?(t) do 35 | t == @opaque_ptr or t == @opaque_array 36 | end 37 | 38 | # 39 | @type t() :: %__MODULE__{ 40 | kind_name: atom(), 41 | zig_t: String.t(), 42 | module_name: atom(), 43 | fields: list(atom()), 44 | kind_functions: list({atom(), integer()}) 45 | } 46 | defstruct kind_name: nil, zig_t: nil, module_name: nil, fields: [], kind_functions: [] 47 | 48 | defp module_basename(%__MODULE__{module_name: module_name}) do 49 | module_name |> Module.split() |> List.last() |> String.to_atom() 50 | end 51 | 52 | defp module_basename("c.struct_" <> struct_name) do 53 | struct_name |> String.to_atom() 54 | end 55 | 56 | defp module_basename("isize") do 57 | :ISize 58 | end 59 | 60 | defp module_basename("usize") do 61 | :USize 62 | end 63 | 64 | defp module_basename("c_int") do 65 | :CInt 66 | end 67 | 68 | defp module_basename("c_uint") do 69 | :CUInt 70 | end 71 | 72 | defp module_basename("[*c]const u8") do 73 | :CString 74 | end 75 | 76 | defp module_basename(@opaque_ptr) do 77 | :OpaquePtr 78 | end 79 | 80 | defp module_basename(@opaque_array) do 81 | :OpaqueArray 82 | end 83 | 84 | # zig 0.9 85 | defp module_basename("?fn(" <> _ = fn_name) do 86 | raise "need module name for function type: #{fn_name}" 87 | end 88 | 89 | # zig 0.10 90 | defp module_basename("?*const fn(" <> _ = fn_name) do 91 | raise "need module name for function type: #{fn_name}" 92 | end 93 | 94 | defp module_basename(type) when is_atom(type) do 95 | type |> Atom.to_string() |> module_basename() 96 | end 97 | 98 | defp module_basename(type) when is_binary(type) do 99 | type |> Macro.camelize() |> String.to_atom() 100 | end 101 | 102 | def default(root_module, type) when is_atom(type) or type in [@opaque_ptr, @opaque_array] do 103 | {:ok, 104 | %__MODULE__{zig_t: type, module_name: Module.concat(root_module, module_basename(type))}} 105 | end 106 | 107 | def default(root_module, type) do 108 | Logger.error( 109 | "Code generation for #{inspect(root_module)} not implemented for type:\n#{inspect(type, pretty: true)}" 110 | ) 111 | 112 | raise "Code gen not implemented" 113 | end 114 | 115 | defp dump_zig_type(@opaque_ptr) do 116 | {:ok, "?*anyopaque"} 117 | end 118 | 119 | defp dump_zig_type(@opaque_array) do 120 | {:ok, "?*const anyopaque"} 121 | end 122 | 123 | defp dump_zig_type(t) when t in @primitive_types do 124 | {:ok, Atom.to_string(t)} 125 | end 126 | 127 | defp dump_zig_type(t) when is_atom(t) do 128 | {:ok, "c." <> Atom.to_string(t)} 129 | end 130 | 131 | defp dump_zig_type(t) when is_atom(t) do 132 | {:ok, "c." <> Atom.to_string(t)} 133 | end 134 | 135 | defp dump_zig_type(_t) do 136 | :error 137 | end 138 | 139 | defp zig_type(%__MODULE__{ 140 | zig_t: zig_t, 141 | kind_name: kind_name 142 | }) do 143 | with {:ok, t} <- dump_zig_type(zig_t) do 144 | t 145 | else 146 | _ -> 147 | "c.#{kind_name}" 148 | end 149 | end 150 | 151 | def gen_resource_kind(%__MODULE__{module_name: module_name, kind_name: kind_name} = k) do 152 | """ 153 | pub const #{kind_name} = kinda.ResourceKind(#{zig_type(k)}, "#{module_name}"); 154 | """ 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kinda 2 | 3 | Kinda is an Elixir package using Zig to bind a C library to BEAM the Erlang virtual machine. 4 | The core idea here is using comptime features in Zig to create a "resource kind" which is "higher-kinded" type abstracts the NIF resource object, C type and Elixir module. 5 | 6 | The general source code generating and building approach here is highly inspired by the TableGen/.inc in LLVM. 7 | Kinda will generate NIFs exported by resource kinds and and provide Elixir macros to generate higher level API to call them and create resource. 8 | With Kinda, NIFs generated and hand-rolled co-exist and complement each other. 9 | 10 | ## Installation 11 | 12 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 13 | by adding `Kinda` to your list of dependencies in `mix.exs`: 14 | 15 | ```elixir 16 | def deps do 17 | [ 18 | {:kinda, "~> 0.7.1"} 19 | ] 20 | end 21 | ``` 22 | 23 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 24 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 25 | be found at . 26 | 27 | ## Usage 28 | 29 | A full example could be found in [kinda_example](kinda_example) 30 | 31 | ### More examples 32 | 33 | - define a root module for the C library 34 | 35 | ```elixir 36 | defmodule Foo.CAPI do 37 | use Kinda.Library, kinds: [Foo.BarKind] 38 | end 39 | ``` 40 | 41 | - define a forwarder module 42 | ```elixir 43 | defmodule Foo.Native do 44 | use Kinda.Forwarder, root_module: CAPI 45 | end 46 | ``` 47 | - define kinds 48 | ```elixir 49 | defmodule Foo.BarKind do 50 | use Kinda.ResourceKind, 51 | forward_module: Foo.Native 52 | end 53 | ``` 54 | 55 | ## What Kinda does 56 | 57 | - Make NIF more of a purely function dispatch. So that you can break the complicity among C/Zig and Elixir. 58 | - Make it possible to pattern matching a C type in Elixir. 59 | - Everything in Kinda could be a NIF resource, including primitive types like integer and C struct. This makes it possible to pass them to C functions as pointers. 60 | - Kinda will generate a NIF function for every C function in your wrapper header, and register every C struct as NIF resource. 61 | 62 | ## Cool features in Kinda enabled by Zig 63 | 64 | - Packing anything into a resource 65 | 66 | Almost all C++/Rust implementation seems to force you to map a fixed size type to a resource type. 67 | In fact for same resource type, you can have Erlang allocate memory of any size. 68 | With Zig's comptime `sizeof` you can easily pack a list of items into an array/struct without adding any abstraction and overhead. An illustration: 69 | 70 | ``` 71 | [(address to item1), item1, item2, item3, ...] 72 | ``` 73 | 74 | So the memory is totally managed by Erlang, and you can use Zig's comptime feature to infer everything involved. 75 | 76 | - Saving lib/include path to a Zig source and use them in your `build.zig`. You can use Elixir to find all the paths. It is way better than configuring with make/CMake because you are using Elixir a whole programming language to do it. It is described in Zig doc as: 77 | 78 | > Surfacing build configuration as comptime values by providing a file that can be imported by Zig code. 79 | 80 | - Inter NIF resource exchange. Because it is Zig, just import the Zig source from another Hex package. 81 | 82 | ## Differences from Zigler 83 | 84 | Kinda borrows a lot of good ideas and code from Zigler (Zigler is awesome~) but there are some differences: 85 | 86 | - Kinda's primary goal is to help you consume a C library, not helping you write NIFs in Zig. 87 | - Kinda expects you to have a `build.zig`. So if you want to also sneak CMake/Bazel inside, go for it. 88 | - In functions generated by Kinda, all memory are allocated and managed by Erlang as resource. 89 | 90 | ## Differences from Rustler 91 | 92 | Kinda is also inspired by Rustler. Rustler really define what a ergonomic NIF lib should be like. 93 | 94 | - Kinda should have the sugar for resource open and NIF declaration similar to Rustler but are provided in Elixir (`nif_gen` and `type_gen`). 95 | - Due to the absence of official Zig package indexing, as for now Kinda's approach could be more of a monolithic NIF lib while in Rustler, you can break things into different crates which is really nice. 96 | - The only protection Kinda might provide is the resource type checks. Lifetime and other more sophisticated checks are expected to be provided by the C library you are consuming. 97 | 98 | ## Differences from TableGen 99 | 100 | - Usually TableGen generates C/C++ source code. While in Kinda it is expected to generate Elixir AST and get compiled directly. 101 | - To generate Zig code, Kinda takes in C `.h` files instead of `.td` files. 102 | 103 | ## Core concepts 104 | 105 | - `ResourceKind`: a Zig struct to bundle: 106 | 107 | - C types 108 | - Erlang NIF resource object type 109 | - functions to open/fetch/make a resource object. 110 | - there could be higher order `ResourceKind` to bundle one or more different `ResourceKind`s 111 | 112 | - `root_module`: the NIF module will load the C shared library 113 | - `forward_module`: module implement functions like `array/2`, `ptr/1` to forward functions to `root_module`. By implementing different callbacks, you might choose to use Elixir struct to wrap a resource or use it directly. 114 | - Recommended module mapping convention: 115 | 116 | - let's say you have a Elixir module to manage a C type. And the NIF module is `SomeLib.CAPI` 117 | 118 | ```elixir 119 | defmodule SomeLib.I64 do 120 | defstruct ref: nil 121 | def create() do 122 | ref = apply(SomeLib.CAPI, Module.concat([__MODULE__, :create]) |> Kinda.check! 123 | struct!(__MODULE__, %{ref: ref}) 124 | end 125 | end 126 | ``` 127 | 128 | - in `SomeLib.CAPI` there should be a NIF generated by Kinda with name `:"Elixir.SomeLib.I64.create"/0` 129 | 130 | - wrapper: a `.h` C header file including all the C types and functions you want to use in Elixir. Kinda will generate a NIF module for every wrapper file. 131 | - wrapped functions, C function with corresponding NIF function name, will be called in the NIF module. 132 | - kinds to generate: return type and argument types of every functions in wrapper will be generated. User will need to implement the behavior `Kinda.CodeGen` for a type with a special name (usually it is C function pointer), or it is the type name in C source by default. 133 | - raw nifs: nifs doesn't follow involved in the resource kind naming convention. Insides these NIFs it is recommended to use NIF resource types registered by Kinda. 134 | 135 | ## Internals 136 | 137 | ### Source code generation 138 | 139 | 1. calling Zig's `translation-c` to generate Zig source code from wrapper header 140 | 2. parse Zig source into ast and then: 141 | 142 | - collect C types declared as constants in Zig (mainly structs), generate kinds for them 143 | - generate kinds for primitive types like int, float, etc. 144 | - generate pointer/array kinds for all types 145 | 146 | 3. generate NIF source for every C function in the wrapper header 147 | 4. generate kind source for every C type used in C functions 148 | 149 | ## Pre-built mode 150 | 151 | - Out of the box Kinda supports generating and loading pre-built NIF library. 152 | 153 | ```elixir 154 | use Kinda.Prebuilt, 155 | otp_app: :beaver, 156 | base_url: "[URL]", 157 | version: "[VERSION]" 158 | ``` 159 | 160 | - It reuses code in [rustler_precompiled](https://github.com/philss/rustler_precompiled.git) to follow the same convention of checksum checks and OTP compatibility rules. 161 | - In Kinda, besides the main NIF library, there might be `kinda-meta-*.ex` for functions signatures and multiple shared libraries the main NIF library depends on. (Zig doesn't support static linking yet so we have to ship shared ones. [Related issue](https://github.com/ziglang/zig/issues/9053)) 162 | 163 | ## Release 164 | 165 | - run the example 166 | 167 | ``` 168 | cd kinda_example 169 | mix deps.get 170 | mix test --force 171 | mix compile --force 172 | ``` 173 | -------------------------------------------------------------------------------- /src/kinda.zig: -------------------------------------------------------------------------------- 1 | pub const beam = @import("beam.zig"); 2 | pub const erl_nif = @cImport({ 3 | @cInclude("erl_nif.h"); 4 | }); 5 | const e = erl_nif; 6 | const std = @import("std"); 7 | pub const result = @import("result.zig"); 8 | 9 | // a function to make a resource term from a u8 slice. 10 | const OpaqueMaker: type = fn (beam.env, []u8) beam.term; 11 | pub const OpaqueStructType = struct { 12 | const Accessor: type = struct { maker: OpaqueMaker, offset: usize }; 13 | const ArrayType = ?*anyopaque; 14 | const PtrType = ?*anyopaque; 15 | storage: std.array_list.AlignedManaged(u8, null) = std.array_list.Managed(u8).init(beam.allocator), 16 | finalized: bool, // if it is finalized, can't append more fields to it. Only finalized struct can be addressed. 17 | accessors: std.ArrayList(Accessor), 18 | }; 19 | 20 | pub const OpaqueField = extern struct { 21 | storage: std.array_list.AlignedManaged(u8, null), 22 | maker: type = OpaqueMaker, 23 | }; 24 | 25 | pub const Internal = struct { 26 | pub const OpaquePtr: type = ResourceKind(?*anyopaque, "Kinda.Internal.OpaquePtr"); 27 | pub const OpaqueArray: type = ResourceKind(?*const anyopaque, "Kinda.Internal.OpaqueArray"); 28 | pub const USize: type = ResourceKind(usize, "Kinda.Internal.USize"); 29 | pub const OpaqueStruct: type = ResourceKind(OpaqueStructType, "Kinda.Internal.OpaqueStruct"); 30 | }; 31 | 32 | pub const numOfNIFsPerKind = 10; 33 | pub fn ResourceKind(comptime ElementType: type, comptime module_name_: anytype) type { 34 | return struct { 35 | pub const module_name = module_name_; 36 | pub const T = ElementType; 37 | const PtrType = if (@typeInfo(ElementType) == .@"struct" and @hasDecl(ElementType, "PtrType")) 38 | ElementType.PtrType 39 | else 40 | [*c]ElementType; // translate-c pointer type 41 | pub const resource = struct { 42 | pub var t: beam.resource_type = undefined; 43 | pub const name = @typeName(ElementType); 44 | pub fn make(env: beam.env, value: T) !beam.term { 45 | return beam.make_resource(env, value, t); 46 | } 47 | pub fn make_kind(env: beam.env, value: T) !beam.term { 48 | var tuple_slice: []beam.term = beam.allocator.alloc(beam.term, 3) catch return beam.Error.@"Fail to allocate memory for tuple slice"; 49 | defer beam.allocator.free(tuple_slice); 50 | tuple_slice[0] = beam.make_atom(env, "kind"); 51 | tuple_slice[1] = beam.make_atom(env, module_name); 52 | const ret = resource.make(env, value) catch return beam.Error.@"Fail to make resource for return type"; 53 | tuple_slice[2] = ret; 54 | return beam.make_tuple(env, tuple_slice); 55 | } 56 | pub fn fetch(env: beam.env, arg: beam.term) !T { 57 | return beam.fetch_resource(T, env, t, arg); 58 | } 59 | pub fn fetch_ptr(env: beam.env, arg: beam.term) !PtrType { 60 | return beam.fetch_resource_ptr(PtrType, env, t, arg); 61 | } 62 | }; 63 | pub const Ptr = struct { 64 | pub const module_name = module_name_ ++ ".Ptr"; 65 | pub const T = PtrType; 66 | pub const resource = struct { 67 | pub var t: beam.resource_type = undefined; 68 | pub const name = @typeName(PtrType); 69 | pub fn make(env: beam.env, value: PtrType) !beam.term { 70 | return beam.make_resource(env, value, t); 71 | } 72 | pub fn fetch(env: beam.env, arg: beam.term) !PtrType { 73 | return beam.fetch_resource(PtrType, env, t, arg); 74 | } 75 | }; 76 | }; 77 | const ArrayType = if (@typeInfo(ElementType) == .@"struct" and @hasDecl(ElementType, "ArrayType")) 78 | ElementType.ArrayType 79 | else 80 | [*c]const ElementType; // translate-c Array type 81 | pub const Array = struct { 82 | pub const module_name = module_name_ ++ ".Array"; 83 | pub const T = ArrayType; 84 | pub const resource = struct { 85 | pub var t: beam.resource_type = undefined; 86 | pub const name = @typeName(ArrayType); 87 | pub fn make(env: beam.env, value: ArrayType) !beam.term { 88 | return beam.make_resource(env, value, t); 89 | } 90 | pub fn fetch(env: beam.env, arg: beam.term) !ArrayType { 91 | return beam.fetch_resource(ArrayType, env, t, arg); 92 | } 93 | }; 94 | // get the array adress as a opaque array 95 | pub fn as_opaque(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 96 | const array_ptr: ArrayType = @This().resource.fetch(env, args[0]) catch 97 | return beam.Error.@"Fail to fetch resource for array"; 98 | return Internal.OpaqueArray.resource.make(env, @ptrCast(array_ptr)) catch 99 | return beam.Error.@"Fail to make resource for opaque array"; 100 | } 101 | }; 102 | fn ptr(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 103 | return beam.get_resource_ptr_from_term(env, @This().PtrType, @This().resource.t, Ptr.resource.t, args[0]) catch return beam.Error.@"Fail to make ptr resource"; 104 | } 105 | fn ptr_to_opaque(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 106 | const typed_ptr: Ptr.T = Ptr.resource.fetch(env, args[0]) catch return beam.Error.@"Fail to fetch ptr resource"; 107 | return Internal.OpaquePtr.resource.make(env, @ptrCast(typed_ptr)) catch return beam.Error.@"Fail to make resource for opaque ptr"; 108 | } 109 | pub fn opaque_ptr(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 110 | const ptr_to_resource_memory: Ptr.T = beam.fetch_resource_ptr(@This().PtrType, env, @This().resource.t, args[0]) catch return beam.Error.@"Fail to fetch ptr resource"; 111 | return Internal.OpaquePtr.resource.make(env, @ptrCast(ptr_to_resource_memory)) catch return beam.Error.@"Fail to make resource for opaque ptr"; 112 | } 113 | // the returned term owns the memory of the array. 114 | fn array(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 115 | return beam.get_resource_array(T, env, @This().resource.t, Array.resource.t, args[0]) catch return beam.Error.@"Fail to make array resource"; 116 | } 117 | // the returned term owns the memory of the array. 118 | // TODO: mut array should be a dedicated resource type without reusing Ptr.resource.t 119 | fn mut_array(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 120 | return beam.get_resource_array(T, env, @This().resource.t, Ptr.resource.t, args[0]) catch beam.Error.@"Fail to make mutable array resource"; 121 | } 122 | fn primitive(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 123 | const v = resource.fetch(env, args[0]) catch return beam.Error.@"Fail to fetch primitive"; 124 | return beam.make(T, env, v) catch return beam.Error.@"Fail to create primitive"; 125 | } 126 | fn dump(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 127 | const v: T = resource.fetch(env, args[0]) catch return beam.Error.@"Fail to fetch primitive"; 128 | var buffer = try std.array_list.Managed(u8).initCapacity(std.heap.page_allocator, 100); 129 | defer buffer.deinit(); 130 | const format_string = switch (@typeInfo(T)) { 131 | .pointer => "{*}\n", 132 | else => "{any}\n", 133 | }; 134 | try std.fmt.format(buffer.writer(), format_string, .{v}); 135 | return beam.make_slice(env, buffer.items); 136 | } 137 | fn append_to_struct(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 138 | const v = resource.fetch(env, args[0]) catch return beam.Error.@"Fail to fetch primitive"; 139 | return beam.make(T, env, v) catch return beam.Error.@"Fail to create primitive"; 140 | } 141 | fn make(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 142 | const v = beam.get(T, env, args[0]) catch return beam.Error.@"Fail to fetch primitive"; 143 | return resource.make(env, v) catch return beam.Error.@"Fail to create primitive"; 144 | } 145 | const OpaquePtrError = error{ @"Fail to fetch resource opaque ptr", failToFetchOffset, @"Fail to allocate memory for tuple slice", @"Fail to make resource for extracted object", @"Fail to make object size" }; 146 | fn make_from_opaque_ptr(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 147 | const ptr_to_read: Internal.OpaquePtr.T = Internal.OpaquePtr.resource.fetch(env, args[0]) catch 148 | return beam.Error.@"Fail to fetch resource opaque ptr"; 149 | const offset: Internal.USize.T = Internal.USize.resource.fetch(env, args[1]) catch 150 | return beam.Error.@"Fail to fetch offset"; 151 | const ptr_int = @intFromPtr(ptr_to_read) + offset; 152 | const obj_ptr: *ElementType = @ptrFromInt(ptr_int); 153 | var tuple_slice: []beam.term = beam.allocator.alloc(beam.term, 2) catch return beam.Error.@"Fail to allocate memory for tuple slice"; 154 | defer beam.allocator.free(tuple_slice); 155 | tuple_slice[0] = resource.make(env, obj_ptr.*) catch return beam.Error.@"Fail to make resource for extracted object"; 156 | tuple_slice[1] = beam.make(Internal.USize.T, env, @sizeOf(ElementType)) catch return beam.Error.@"Fail to make object size"; 157 | return beam.make_tuple(env, tuple_slice); 158 | } 159 | const maker = if (@typeInfo(ElementType) == .@"struct" and @hasDecl(ElementType, "maker")) 160 | ElementType.maker 161 | else 162 | .{ make, 1 }; 163 | const ptr_maker = if (@typeInfo(ElementType) == .@"struct" and @hasDecl(ElementType, "ptr")) 164 | ElementType.ptr 165 | else 166 | ptr; 167 | const extra_nifs = if (@typeInfo(ElementType) == .@"struct" and @hasDecl(ElementType, "nifs")) 168 | ElementType.nifs 169 | else 170 | .{}; 171 | pub const nifs: [numOfNIFsPerKind + @typeInfo(@TypeOf(extra_nifs)).@"struct".fields.len]e.ErlNifFunc = .{ 172 | result.nif(module_name ++ ".ptr", 1, ptr_maker).entry, 173 | result.nif(module_name ++ ".ptr_to_opaque", 1, ptr_to_opaque).entry, 174 | result.nif(module_name ++ ".opaque_ptr", 1, opaque_ptr).entry, 175 | result.nif(module_name ++ ".array", 1, array).entry, 176 | result.nif(module_name ++ ".mut_array", 1, mut_array).entry, 177 | result.nif(module_name ++ ".primitive", 1, primitive).entry, 178 | result.nif(module_name ++ ".make", maker[1], maker[0]).entry, 179 | result.nif(module_name ++ ".dump", 1, dump).entry, 180 | result.nif(module_name ++ ".make_from_opaque_ptr", 2, make_from_opaque_ptr).entry, 181 | result.nif(module_name ++ ".array_as_opaque", 1, @This().Array.as_opaque).entry, 182 | } ++ extra_nifs; 183 | pub fn open(env: beam.env) void { 184 | const dtor = if (@typeInfo(ElementType) == .@"struct" and @hasDecl(ElementType, "destroy")) 185 | ElementType.destroy 186 | else 187 | beam.destroy_do_nothing; 188 | @This().resource.t = e.enif_open_resource_type(env, null, @This().resource.name, dtor, e.ERL_NIF_RT_CREATE | e.ERL_NIF_RT_TAKEOVER, null); 189 | if (@typeInfo(ElementType) == .@"struct" and @hasDecl(ElementType, "resource_type")) { 190 | ElementType.resource_type = @This().resource.t; 191 | } 192 | } 193 | pub fn open_ptr(env: beam.env) void { 194 | @This().Ptr.resource.t = e.enif_open_resource_type(env, null, @This().Ptr.resource.name, beam.destroy_do_nothing, e.ERL_NIF_RT_CREATE | e.ERL_NIF_RT_TAKEOVER, null); 195 | } 196 | pub fn open_array(env: beam.env) void { 197 | // TODO: use a ArrayList/BoundedArray to store the array and deinit it in destroy callback 198 | @This().Array.resource.t = e.enif_open_resource_type(env, null, @This().Array.resource.name, beam.destroy_do_nothing, e.ERL_NIF_RT_CREATE | e.ERL_NIF_RT_TAKEOVER, null); 199 | } 200 | pub fn open_all(env: beam.env) void { 201 | open(env); 202 | open_ptr(env); 203 | open_array(env); 204 | } 205 | }; 206 | } 207 | 208 | pub fn ResourceKind2(comptime ElementType: type) type { 209 | return ResourceKind(ElementType, ElementType.module_name); 210 | } 211 | 212 | pub fn aliasKind(comptime AliasKind: type, comptime Kind: type) void { 213 | AliasKind.resource.t = Kind.resource.t; 214 | AliasKind.Ptr.resource.t = Kind.Ptr.resource.t; 215 | AliasKind.Array.resource.t = Kind.Array.resource.t; 216 | } 217 | 218 | pub fn open_internal_resource_types(env: beam.env) void { 219 | Internal.USize.open_all(env); 220 | Internal.OpaquePtr.open_all(env); 221 | Internal.OpaqueArray.open_all(env); 222 | } 223 | 224 | const NIFFuncAttrs = struct { flags: u32 = 0, nif_name: ?[*c]const u8 = null }; 225 | 226 | // wrap a c function to a bang nif 227 | pub fn BangFunc(comptime Kinds: anytype, c: anytype, comptime name: anytype) type { 228 | @setEvalBranchQuota(5000); 229 | const cfunction = @field(c, name); 230 | const FTI = @typeInfo(@TypeOf(cfunction)).@"fn"; 231 | return (struct { 232 | pub const arity = FTI.params.len; 233 | fn getKind(comptime t: type) type { 234 | for (Kinds) |kind| { 235 | switch (@typeInfo(t)) { 236 | .pointer => { 237 | if (t == kind.Ptr.T) { 238 | return kind.Ptr; 239 | } 240 | if (t == kind.Array.T) { 241 | return kind.Array; 242 | } 243 | if (t == kind.T) { 244 | return kind; 245 | } 246 | }, 247 | else => { 248 | if (t == kind.T) { 249 | return kind; 250 | } 251 | }, 252 | } 253 | } 254 | @compileError("resouce kind not found " ++ @typeName(t)); 255 | } 256 | inline fn VariadicArgs() type { 257 | const P = FTI.params; 258 | return switch (P.len) { 259 | 0 => struct {}, 260 | 1 => struct { P[0].type.? }, 261 | 2 => struct { P[0].type.?, P[1].type.? }, 262 | 3 => struct { P[0].type.?, P[1].type.?, P[2].type.? }, 263 | 4 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.? }, 264 | 5 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.? }, 265 | 6 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.? }, 266 | 7 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.?, P[6].type.? }, 267 | 8 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.?, P[6].type.?, P[7].type.? }, 268 | 9 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.?, P[6].type.?, P[7].type.?, P[8].type.? }, 269 | 10 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.?, P[6].type.?, P[7].type.?, P[8].type.?, P[9].type.? }, 270 | 11 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.?, P[6].type.?, P[7].type.?, P[8].type.?, P[9].type.?, P[10].type.? }, 271 | 12 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.?, P[6].type.?, P[7].type.?, P[8].type.?, P[9].type.?, P[10].type.?, P[11].type.? }, 272 | 13 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.?, P[6].type.?, P[7].type.?, P[8].type.?, P[9].type.?, P[10].type.?, P[11].type.?, P[12].type.? }, 273 | 14 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.?, P[6].type.?, P[7].type.?, P[8].type.?, P[9].type.?, P[10].type.?, P[11].type.?, P[12].type.?, P[13].type.? }, 274 | 15 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.?, P[6].type.?, P[7].type.?, P[8].type.?, P[9].type.?, P[10].type.?, P[11].type.?, P[12].type.?, P[13].type.?, P[14].type.? }, 275 | 16 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.?, P[6].type.?, P[7].type.?, P[8].type.?, P[9].type.?, P[10].type.?, P[11].type.?, P[12].type.?, P[13].type.?, P[14].type.?, P[15].type.? }, 276 | 17 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.?, P[6].type.?, P[7].type.?, P[8].type.?, P[9].type.?, P[10].type.?, P[11].type.?, P[12].type.?, P[13].type.?, P[14].type.?, P[15].type.?, P[16].type.? }, 277 | 18 => struct { P[0].type.?, P[1].type.?, P[2].type.?, P[3].type.?, P[4].type.?, P[5].type.?, P[6].type.?, P[7].type.?, P[8].type.?, P[9].type.?, P[10].type.?, P[11].type.?, P[12].type.?, P[13].type.?, P[14].type.?, P[15].type.?, P[16].type.?, P[17].type.? }, 278 | else => @compileError("too many args"), 279 | }; 280 | } 281 | inline fn variadic_call(args: anytype) FTI.return_type.? { 282 | const f = cfunction; 283 | return switch (FTI.params.len) { 284 | 0 => f(), 285 | 1 => f(args[0]), 286 | 2 => f(args[0], args[1]), 287 | 3 => f(args[0], args[1], args[2]), 288 | 4 => f(args[0], args[1], args[2], args[3]), 289 | 5 => f(args[0], args[1], args[2], args[3], args[4]), 290 | 6 => f(args[0], args[1], args[2], args[3], args[4], args[5]), 291 | 7 => f(args[0], args[1], args[2], args[3], args[4], args[5], args[6]), 292 | 8 => f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]), 293 | 9 => f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]), 294 | 10 => f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]), 295 | 11 => f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10]), 296 | 12 => f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11]), 297 | 13 => f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12]), 298 | 14 => f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13]), 299 | 15 => f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14]), 300 | 16 => f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15]), 301 | 17 => f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16]), 302 | 18 => f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17]), 303 | else => @compileError("too many args"), 304 | }; 305 | } 306 | pub fn wrap_ret_call(env: beam.env, args: anytype) !beam.term { 307 | const rt = FTI.return_type.?; 308 | const RetKind = getKind(rt); 309 | var tuple_slice: []beam.term = beam.allocator.alloc(beam.term, 3) catch return beam.Error.@"Fail to allocate memory for tuple slice"; 310 | defer beam.allocator.free(tuple_slice); 311 | tuple_slice[0] = beam.make_atom(env, "kind"); 312 | tuple_slice[1] = beam.make_atom(env, RetKind.module_name); 313 | const ret = RetKind.resource.make(env, @call(.auto, variadic_call, .{args})) catch return beam.Error.@"Fail to make resource for return type"; 314 | tuple_slice[2] = ret; 315 | return beam.make_tuple(env, tuple_slice); 316 | } 317 | pub fn nif(env: beam.env, _: c_int, args: [*c]const beam.term) !beam.term { 318 | var c_args: VariadicArgs() = undefined; 319 | inline for (FTI.params, args, 0..) |p, arg, i| { 320 | const ArgKind = getKind(p.type.?); 321 | c_args[i] = ArgKind.resource.fetch(env, arg) catch return @field(beam.ArgumentError, "Fail to fetch argument #" ++ std.fmt.comptimePrint("{d}", .{i + 1})); 322 | } 323 | const rt = FTI.return_type.?; 324 | if (rt == void) { 325 | variadic_call(c_args); 326 | return beam.make_ok(env); 327 | } else { 328 | return wrap_ret_call(env, c_args); 329 | } 330 | } 331 | }); 332 | } 333 | 334 | pub fn NIFFunc(comptime Kinds: anytype, c: anytype, comptime name: anytype, comptime attrs: NIFFuncAttrs) e.ErlNifFunc { 335 | const bang = BangFunc(Kinds, c, name); 336 | return result.nif_with_flags(attrs.nif_name orelse name, bang.arity, bang.nif, attrs.flags).entry; 337 | } 338 | -------------------------------------------------------------------------------- /src/beam.zig: -------------------------------------------------------------------------------- 1 | //! This struct contains adapters designed to facilitate interfacing the 2 | //! BEAM's c-style helpers for NIFs with a more idiomatic Zig-style of 3 | //! programming, for example, the use of slices instead of null-terminated 4 | //! arrays as strings. 5 | //! 6 | //! This struct derives from `zig/beam/beam.zig`, and you may import it into 7 | //! your module's zig code by calling: 8 | //! 9 | //! ``` 10 | //! const beam = @import("beam.zig") 11 | //! ``` 12 | //! 13 | //! This is done automatically for you inside your `~Z` forms, so do NOT 14 | //! use this import statement with inline Zig. 15 | //! 16 | //! ## Features 17 | //! 18 | //! ### The BEAM Allocator 19 | //! 20 | //! Wraps `e.enif_alloc` and `e.enif_free` functions into a compliant Zig 21 | //! allocator struct. You should thus be able to supply Zig standard library 22 | //! functions which require an allocator a struct that is compliant with its 23 | //! requirements. 24 | //! 25 | //! This is, in particular, useful for slice generation. 26 | //! 27 | //! #### Example (slice generation) 28 | //! 29 | //! ``` 30 | //! beam = @import("beam.zig"); 31 | //! 32 | //! fn make_a_slice_of_floats() ![]f32 { 33 | //! return beam.allocator.alloc(f32, 100); 34 | //! } 35 | //! ``` 36 | //! 37 | //! Because Zig features *composable allocators*, you can very easily implement 38 | //! custom allocators on top of the existing BEAM allocator. 39 | //! 40 | //! ### Getters 41 | //! 42 | //! Erlang's NIF interface provides a comprehensive set of methods to retrieve 43 | //! data out of BEAM terms. However, this set of methods presents an error 44 | //! handling scheme that is designed for C and inconsistent with the idiomatic 45 | //! scheme used for Zig best practices. 46 | //! 47 | //! A series of get functions is provided, implementing these methods in 48 | //! accordance to best practices. These include `get/3`, which is the generic 49 | //! method for getting scalar values, `get_X`, which are typed methods for 50 | //! retrieving scalar values, and `get_slice_of/3`, which is the generic method 51 | //! for retrieving a Zig slice from a BEAM list. 52 | //! 53 | //! Naturally, for all of these functions, you will have to provide the BEAM 54 | //! environment value. 55 | //! 56 | //! #### Examples 57 | //! 58 | //! ``` 59 | //! const beam = @import("beam.zig"); 60 | //! 61 | //! fn double_value(env: beam.env, value: beam.term) !f64 { 62 | //! return (try beam.get_f64(env, value)) * 2; 63 | //! } 64 | //! 65 | //! fn sum_float_list(env: beam.env, list: beam.term) !f64 { 66 | //! zig_list: []f64 = try beam.get_slice_of(f64, env, list); 67 | //! defer beam.allocator.free(zig_list); // don't forget to clean up! 68 | //! 69 | //! result: f64 = 0; 70 | //! for (list) |item| { result += item; } 71 | //! return result; 72 | //! } 73 | //! ``` 74 | //! 75 | //! ### Makers 76 | //! 77 | //! A series of "make" functions is provided which allow for easy export of 78 | //! Zig values back to the BEAM. Typically, these functions are used in the 79 | //! automatic type marshalling performed by Zigler, however, you may want to 80 | //! be able to use them yourself to assemble BEAM datatypes not directly 81 | //! supported by Zig. For example, a custom tuple value. 82 | //! 83 | //! #### Example 84 | //! 85 | //! ``` 86 | //! const beam = @import("beam.zig"); 87 | //! 88 | //! const ok_slice="ok"[0..]; 89 | //! fn to_ok_tuple(env: beam.env, value: i64) !beam.term { 90 | //! var tuple_slice: []term = try beam.allocator.alloc(beam.term, 2); 91 | //! defer beam.allocator.free(tuple_slice); 92 | //! 93 | //! tuple_slice[0] = beam.make_atom(env, ok_slice); 94 | //! tuple_slice[1] = beam.make_i64(env, value); 95 | //! 96 | //! return beam.make_tuple(env, tuple_slice); 97 | //! } 98 | //! 99 | //! ``` 100 | 101 | const e = @import("kinda.zig").erl_nif; 102 | const std = @import("std"); 103 | const builtin = @import("builtin"); 104 | 105 | /////////////////////////////////////////////////////////////////////////////// 106 | // BEAM allocator definitions 107 | /////////////////////////////////////////////////////////////////////////////// 108 | 109 | const Allocator = std.mem.Allocator; 110 | 111 | // basic allocator 112 | 113 | /// !value 114 | /// provides a default BEAM allocator. This is an implementation of the Zig 115 | /// allocator interface. Use `beam.allocator.alloc` everywhere to safely 116 | /// allocate slices efficiently, and use `beam.allocator.free` to release that 117 | /// memory. For single item allocation, use `beam.allocator.create` and 118 | /// `beam.allocator.destroy` to release the memory. 119 | /// 120 | /// Note this does not make the allocated memory *garbage collected* by the 121 | /// BEAM. 122 | /// 123 | /// All memory will be tracked by the beam. All allocations happen with 8-byte 124 | /// alignment, as described in `erl_nif.h`. This is sufficient to create 125 | /// correctly aligned `beam.terms`, and for most purposes. 126 | /// For data that require greater alignment, use `beam.large_allocator`. 127 | /// 128 | /// ### Example 129 | /// 130 | /// The following code will return ten bytes of new memory. 131 | /// 132 | /// ```zig 133 | /// const beam = @import("beam.zig"); 134 | /// 135 | /// fn give_me_ten_bytes() ![]u8 { 136 | /// return beam.allocator.alloc(u8, 10); 137 | /// } 138 | /// ``` 139 | /// 140 | /// currently does not release memory that is resized. For this behaviour, use 141 | /// use `beam.general_purpose_allocator`. 142 | /// 143 | /// not threadsafe. for a threadsafe allocator, use `beam.general_purpose_allocator` 144 | pub const allocator = raw_beam_allocator; 145 | 146 | pub const MAX_ALIGN: mem.Alignment = .@"8"; 147 | 148 | const raw_beam_allocator = Allocator{ 149 | .ptr = undefined, 150 | .vtable = &raw_beam_allocator_vtable, 151 | }; 152 | const raw_beam_allocator_vtable = Allocator.VTable{ .alloc = raw_beam_alloc, .resize = raw_beam_resize, .free = raw_beam_free, .remap = remap }; 153 | 154 | fn raw_beam_alloc(_: *anyopaque, len: usize, ptr_align: mem.Alignment, _: usize) ?[*]u8 { 155 | if (ptr_align.compare(.gt, MAX_ALIGN)) { 156 | return null; 157 | } 158 | const ptr = e.enif_alloc(len) orelse return null; 159 | return @as([*]u8, @ptrCast(ptr)); 160 | } 161 | 162 | fn raw_beam_resize(_: *anyopaque, buf: []u8, _: mem.Alignment, new_len: usize, _: usize) bool { 163 | if (new_len == 0) { 164 | e.enif_free(buf.ptr); 165 | return true; 166 | } 167 | if (new_len <= buf.len) { 168 | return true; 169 | } 170 | return false; 171 | } 172 | 173 | fn raw_beam_free(_: *anyopaque, buf: []u8, _: mem.Alignment, _: usize) void { 174 | e.enif_free(buf.ptr); 175 | } 176 | 177 | /// !value 178 | /// provides a BEAM allocator that can perform allocations with greater 179 | /// alignment than the machine word. Note that this comes at the cost 180 | /// of some memory to store important metadata. 181 | /// 182 | /// currently does not release memory that is resized. For this behaviour 183 | /// use `beam.general_purpose_allocator`. 184 | /// 185 | /// not threadsafe. for a threadsafe allocator, use `beam.general_purpose_allocator` 186 | pub const large_allocator = large_beam_allocator; 187 | 188 | const large_beam_allocator = Allocator{ 189 | .ptr = undefined, 190 | .vtable = &large_beam_allocator_vtable, 191 | }; 192 | const large_beam_allocator_vtable = Allocator.VTable{ .alloc = large_beam_alloc, .resize = large_beam_resize, .free = Allocator.NoOpFree(anyopaque).noOpFree, .remap = remap }; 193 | 194 | fn remap(context: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, return_address: usize) ?[*]u8 { 195 | // can't use realloc directly because it might not respect alignment. 196 | return if (raw_beam_resize(context, memory, alignment, new_len, return_address)) memory.ptr else null; 197 | } 198 | 199 | fn large_beam_alloc(_: *anyopaque, len: usize, alignment: u29, len_align: u29, return_address: usize) error{OutOfMemory}![]u8 { 200 | var ptr = try alignedAlloc(len, alignment, len_align, return_address); 201 | if (len_align == 0) { 202 | return ptr[0..len]; 203 | } 204 | return ptr[0..std.mem.alignBackwardAnyAlign(len, len_align)]; 205 | } 206 | 207 | fn large_beam_resize( 208 | _: *anyopaque, 209 | buf: []u8, 210 | buf_align: u29, 211 | new_len: usize, 212 | len_align: u29, 213 | _: usize, 214 | ) ?usize { 215 | if (new_len > buf.len) { 216 | return null; 217 | } 218 | if (new_len == 0) { 219 | return alignedFree(buf, buf_align); 220 | } 221 | if (len_align == 0) { 222 | return new_len; 223 | } 224 | return std.mem.alignBackwardAnyAlign(new_len, len_align); 225 | } 226 | 227 | fn alignedAlloc(len: usize, alignment: u29, _: u29, _: usize) ![*]u8 { 228 | const safe_len = safeLen(len, alignment); 229 | const alloc_slice: []u8 = try allocator.allocAdvanced(u8, MAX_ALIGN, safe_len, std.mem.Allocator.Exact.exact); 230 | 231 | const unaligned_addr = @intFromPtr(alloc_slice.ptr); 232 | const aligned_addr = reAlign(unaligned_addr, alignment); 233 | 234 | getPtrPtr(aligned_addr).* = unaligned_addr; 235 | return aligned_addr; 236 | } 237 | 238 | fn alignedFree(buf: []u8, alignment: u29) usize { 239 | const ptr = getPtrPtr(buf.ptr).*; 240 | allocator.free(@as([*]u8, @ptrFromInt(ptr))[0..safeLen(buf.len, alignment)]); 241 | return 0; 242 | } 243 | 244 | fn reAlign(unaligned_addr: usize, alignment: u29) [*]u8 { 245 | return @ptrFromInt(std.mem.alignForward(unaligned_addr + @sizeOf(usize), alignment)); 246 | } 247 | 248 | fn safeLen(len: usize, alignment: u29) usize { 249 | return len + alignment - @sizeOf(usize) + MAX_ALIGN; 250 | } 251 | 252 | fn getPtrPtr(aligned_ptr: [*]u8) *usize { 253 | return @ptrFromInt(@intFromPtr(aligned_ptr) - @sizeOf(usize)); 254 | } 255 | 256 | /// !value 257 | /// wraps the zig GeneralPurposeAllocator into the standard BEAM allocator. 258 | var general_purpose_allocator_instance = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){ 259 | .backing_allocator = large_allocator, 260 | }; 261 | 262 | pub var general_purpose_allocator = general_purpose_allocator_instance.allocator(); 263 | 264 | /////////////////////////////////////////////////////////////////////////////// 265 | // syntactic sugar: important elixir terms 266 | /////////////////////////////////////////////////////////////////////////////// 267 | 268 | /// errors for nif translation 269 | pub const Error = 270 | error{ @"Function clause error", @"Fail to make resource", @"Fail to fetch resource", @"Fail to fetch resource ptr", @"Fail to fetch resource for array", @"Fail to fetch resource list element", @"Fail to make resource for opaque array", @"Fail to fetch primitive", @"Fail to create primitive", @"Fail to make resource for return type", @"Fail to allocate memory for tuple slice", @"Fail to make ptr resource", @"Fail to fetch ptr resource", @"Fail to make resource for opaque ptr", @"Fail to make array resource", @"Fail to make mutable array resource", @"Fail to fetch resource opaque ptr", @"Fail to fetch offset", @"Fail to make resource for extracted object", @"Fail to make object size", @"Fail to inspect resource binary", @"Fail to get boolean", @"Fail to get calling process" }; 271 | 272 | pub const ArgumentError = error{ 273 | @"Fail to fetch argument #1", 274 | @"Fail to fetch argument #2", 275 | @"Fail to fetch argument #3", 276 | @"Fail to fetch argument #4", 277 | @"Fail to fetch argument #5", 278 | @"Fail to fetch argument #6", 279 | @"Fail to fetch argument #7", 280 | @"Fail to fetch argument #8", 281 | @"Fail to fetch argument #9", 282 | @"Fail to fetch argument #10", 283 | @"Fail to fetch argument #11", 284 | @"Fail to fetch argument #12", 285 | @"Fail to fetch argument #13", 286 | @"Fail to fetch argument #14", 287 | @"Fail to fetch argument #15", 288 | @"Fail to fetch argument #16", 289 | @"Fail to fetch argument #17", 290 | @"Fail to fetch argument #18", 291 | }; 292 | 293 | /// errors for launching nif errors 294 | /// LaunchError Occurs when there's a problem launching a threaded nif. 295 | pub const ThreadError = error{LaunchError}; 296 | 297 | /// syntactic sugar for the BEAM environment. Note that the `env` type 298 | /// encapsulates the pointer, since you will almost always be passing this 299 | /// pointer to an opaque struct around without accessing it. 300 | pub const env = ?*e.ErlNifEnv; 301 | 302 | /// syntactic sugar for the BEAM term struct (`e.ERL_NIF_TERM`) 303 | pub const term = e.ERL_NIF_TERM; 304 | 305 | /////////////////////////////////////////////////////////////////////////////// 306 | // syntactic sugar: gets 307 | /////////////////////////////////////////////////////////////////////////////// 308 | 309 | /////////////////////////////////////////////////////////////////////////////// 310 | // generics 311 | 312 | /// A helper for marshalling values from the BEAM runtime into Zig. Use this 313 | /// function if you need support for Zig generics. 314 | /// 315 | /// Used internally to typeheck values coming into Zig slice. 316 | /// 317 | /// supported types: 318 | /// - `c_int` 319 | /// - `c_long` 320 | /// - `isize` 321 | /// - `usize` 322 | /// - `u8` 323 | /// - `i32` 324 | /// - `i64` 325 | /// - `f16` 326 | /// - `f32` 327 | /// - `f64` 328 | pub fn get(comptime T: type, env_: env, value: term) !T { 329 | switch (T) { 330 | c_int => return get_c_int(env_, value), 331 | c_uint => return get_c_uint(env_, value), 332 | c_long => return get_c_long(env_, value), 333 | isize => return get_isize(env_, value), 334 | usize => return get_usize(env_, value), 335 | u8 => return get_u8(env_, value), 336 | u16 => return get_u16(env_, value), 337 | u32 => return get_u32(env_, value), 338 | u64 => return get_u64(env_, value), 339 | i8 => return get_i8(env_, value), 340 | i16 => return get_i16(env_, value), 341 | i32 => return get_i32(env_, value), 342 | i64 => return get_i64(env_, value), 343 | f16 => return get_f16(env_, value), 344 | f32 => return get_f32(env_, value), 345 | f64 => return get_f64(env_, value), 346 | bool => return get_bool(env_, value), 347 | else => return Error.@"Function clause error", 348 | } 349 | } 350 | 351 | /////////////////////////////////////////////////////////////////////////////// 352 | // ints 353 | 354 | /// Takes a BEAM int term and returns a `c_int` value. Should only be used for 355 | /// C interop with Zig functions. 356 | /// 357 | pub fn get_c_int(environment: env, src_term: term) !c_int { 358 | var result: c_int = undefined; 359 | if (0 != e.enif_get_int(environment, src_term, &result)) { 360 | return result; 361 | } else { 362 | return Error.@"Function clause error"; 363 | } 364 | } 365 | 366 | /// Takes a BEAM int term and returns a `c_uint` value. Should only be used for 367 | /// C interop with Zig functions. 368 | /// 369 | pub fn get_c_uint(environment: env, src_term: term) !c_uint { 370 | var result: c_uint = undefined; 371 | if (0 != e.enif_get_uint(environment, src_term, &result)) { 372 | return result; 373 | } else { 374 | return Error.@"Function clause error"; 375 | } 376 | } 377 | 378 | /// Takes a BEAM int term and returns a `c_long` value. Should only be used 379 | /// for C interop with Zig functions. 380 | /// 381 | pub fn get_c_long(environment: env, src_term: term) !c_long { 382 | var result: c_long = undefined; 383 | if (0 != e.enif_get_long(environment, src_term, &result)) { 384 | return result; 385 | } else { 386 | return Error.@"Function clause error"; 387 | } 388 | } 389 | 390 | /// Takes a BEAM int term and returns a `c_ulong` value. Should only be used 391 | /// for C interop with Zig functions. 392 | /// 393 | pub fn get_c_ulong(environment: env, src_term: term) !c_ulong { 394 | var result: c_ulong = undefined; 395 | if (0 != e.enif_get_ulong(environment, src_term, &result)) { 396 | return result; 397 | } else { 398 | return Error.@"Function clause error"; 399 | } 400 | } 401 | 402 | /// Takes a BEAM int term and returns a `isize` value. Should only be used 403 | /// for C interop. 404 | /// 405 | pub fn get_isize(environment: env, src_term: term) !isize { 406 | var result: i64 = undefined; 407 | if (0 != e.enif_get_long(environment, src_term, @ptrCast(&result))) { 408 | return @intCast(result); 409 | } else { 410 | return Error.@"Function clause error"; 411 | } 412 | } 413 | 414 | /// Takes a BEAM int term and returns a `usize` value. Zig idiomatically uses 415 | /// `usize` for its size values, so typically you should be using this function. 416 | /// 417 | pub fn get_usize(environment: env, src_term: term) !usize { 418 | var result: i64 = undefined; 419 | if (0 != e.enif_get_long(environment, src_term, @ptrCast(&result))) { 420 | return @intCast(result); 421 | } else { 422 | return Error.@"Function clause error"; 423 | } 424 | } 425 | 426 | /// Takes a BEAM int term and returns a `u8` value. 427 | /// 428 | /// Note that this conversion function checks to make sure it's in range 429 | /// (`0..255`). 430 | /// 431 | pub fn get_u8(environment: env, src_term: term) !u8 { 432 | var result: c_int = undefined; 433 | if (0 != e.enif_get_int(environment, src_term, &result)) { 434 | if ((result >= 0) and (result <= 0xFF)) { 435 | return @intCast(result); 436 | } else { 437 | return Error.@"Function clause error"; 438 | } 439 | } else { 440 | return Error.@"Function clause error"; 441 | } 442 | } 443 | 444 | /// Takes a BEAM int term and returns a `u16` value. 445 | /// 446 | /// Note that this conversion function checks to make sure it's in range 447 | /// (`0..65535`). 448 | /// 449 | pub fn get_u16(environment: env, src_term: term) !u16 { 450 | var result: c_int = undefined; 451 | if (0 != e.enif_get_int(environment, src_term, &result)) { 452 | if ((result >= 0) and (result <= 0xFFFF)) { 453 | return @intCast(result); 454 | } else { 455 | return Error.@"Function clause error"; 456 | } 457 | } else { 458 | return Error.@"Function clause error"; 459 | } 460 | } 461 | 462 | /// Takes a BEAM int term and returns a `u32` value. 463 | /// 464 | pub fn get_u32(environment: env, src_term: term) !u32 { 465 | var result: c_uint = undefined; 466 | if (0 != e.enif_get_uint(environment, src_term, &result)) { 467 | return @intCast(result); 468 | } else { 469 | return Error.@"Function clause error"; 470 | } 471 | } 472 | 473 | /// Takes a BEAM int term and returns a `u64` value. 474 | /// 475 | pub fn get_u64(environment: env, src_term: term) !u64 { 476 | var result: c_ulong = undefined; 477 | if (0 != e.enif_get_ulong(environment, src_term, &result)) { 478 | return @intCast(result); 479 | } else { 480 | return Error.@"Function clause error"; 481 | } 482 | } 483 | 484 | /// Takes a BEAM int term and returns an `i8` value. 485 | /// 486 | /// Note that this conversion function checks to make sure it's in range 487 | /// (`-128..127`). 488 | /// 489 | pub fn get_i8(environment: env, src_term: term) !i8 { 490 | var result: c_int = undefined; 491 | if (0 != e.enif_get_int(environment, src_term, &result)) { 492 | if ((result >= -128) and (result <= 127)) { 493 | return @intCast(result); 494 | } else { 495 | return Error.@"Function clause error"; 496 | } 497 | } else { 498 | return Error.@"Function clause error"; 499 | } 500 | } 501 | 502 | /// Takes a BEAM int term and returns an `i16` value. 503 | /// 504 | /// Note that this conversion function checks to make sure it's in range 505 | /// (`-32768..32767`). 506 | /// 507 | pub fn get_i16(environment: env, src_term: term) !i16 { 508 | var result: c_int = undefined; 509 | if (0 != e.enif_get_int(environment, src_term, &result)) { 510 | if ((result >= -32768) and (result <= 32767)) { 511 | return @intCast(result); 512 | } else { 513 | return Error.@"Function clause error"; 514 | } 515 | } else { 516 | return Error.@"Function clause error"; 517 | } 518 | } 519 | 520 | /// Takes a BEAM int term and returns an `i32` value. 521 | /// 522 | /// Note that this conversion function does not currently do range checking. 523 | /// 524 | pub fn get_i32(environment: env, src_term: term) !i32 { 525 | var result: c_int = undefined; 526 | if (0 != e.enif_get_int(environment, src_term, &result)) { 527 | return @intCast(result); 528 | } else { 529 | return Error.@"Function clause error"; 530 | } 531 | } 532 | 533 | /// Takes a BEAM int term and returns an `i64` value. 534 | /// 535 | /// Note that this conversion function does not currently do range checking. 536 | /// 537 | pub fn get_i64(environment: env, src_term: term) !i64 { 538 | var result: i64 = undefined; 539 | if (0 != e.enif_get_long(environment, src_term, @ptrCast(&result))) { 540 | return result; 541 | } else { 542 | return Error.@"Function clause error"; 543 | } 544 | } 545 | 546 | /////////////////////////////////////////////////////////////////////////////// 547 | // floats 548 | 549 | /// Takes a BEAM float term and returns an `f16` value. 550 | /// 551 | /// Note that this conversion function does not currently do range checking. 552 | /// 553 | pub fn get_f16(environment: env, src_term: term) !f16 { 554 | var result: f64 = undefined; 555 | if (0 != e.enif_get_double(environment, src_term, &result)) { 556 | return @floatCast(result); 557 | } else { 558 | return Error.@"Function clause error"; 559 | } 560 | } 561 | 562 | /// Takes a BEAM float term and returns an `f32` value. 563 | /// 564 | /// Note that this conversion function does not currently do range checking. 565 | /// 566 | pub fn get_f32(environment: env, src_term: term) !f32 { 567 | var result: f64 = undefined; 568 | if (0 != e.enif_get_double(environment, src_term, &result)) { 569 | return @floatCast(result); 570 | } else { 571 | return Error.@"Function clause error"; 572 | } 573 | } 574 | 575 | /// Takes a BEAM float term and returns an `f64` value. 576 | /// 577 | pub fn get_f64(environment: env, src_term: term) !f64 { 578 | var result: f64 = undefined; 579 | if (0 != e.enif_get_double(environment, src_term, &result)) { 580 | return result; 581 | } else { 582 | return Error.@"Function clause error"; 583 | } 584 | } 585 | 586 | /////////////////////////////////////////////////////////////////////////////// 587 | // atoms 588 | 589 | /// note that Zig has no equivalent of a BEAM atom, so we will just declare 590 | /// it as a term. You can retrieve the string value of the BEAM atom using 591 | /// `get_atom_slice/2` 592 | pub const atom = term; 593 | 594 | const __latin1 = e.ERL_NIF_LATIN1; 595 | 596 | /// Takes a BEAM atom term and retrieves it as a slice `[]u8` value. 597 | /// it's the caller's responsibility to make sure that the value is freed. 598 | /// 599 | /// Uses the standard `beam.allocator` allocator. If you require a custom 600 | /// allocator, use `get_atom_slice_alloc/3` 601 | /// 602 | pub fn get_atom_slice(environment: env, src_term: atom) ![]u8 { 603 | return get_atom_slice_alloc(allocator, environment, src_term); 604 | } 605 | 606 | /// Takes a BEAM atom term and retrieves it as a slice `[]u8` value, with 607 | /// any allocator. 608 | /// 609 | pub fn get_atom_slice_alloc(a: Allocator, environment: env, src_term: atom) ![]u8 { 610 | var len: c_uint = undefined; 611 | var result: []u8 = undefined; 612 | if (0 != e.enif_get_atom_length(environment, src_term, @ptrCast(&len), __latin1)) { 613 | result = try a.alloc(u8, len + 1); 614 | 615 | // pull the value from the beam. 616 | if (0 != e.enif_get_atom(environment, src_term, @ptrCast(&result[0]), len + 1, __latin1)) { 617 | // trim the slice, it's the caller's responsibility to free it. 618 | return result[0..len]; 619 | } else { 620 | unreachable; 621 | } 622 | } else { 623 | return Error.@"Function clause error"; 624 | } 625 | } 626 | 627 | /////////////////////////////////////////////////////////////////////////////// 628 | // binaries 629 | 630 | /// shorthand for `e.ErlNifBinary`. 631 | pub const binary = e.ErlNifBinary; 632 | 633 | /// Takes an BEAM `t:binary/0` term and retrieves a pointer to the 634 | /// binary data as a Zig c-string (`[*c]u8`). No memory is allocated for 635 | /// this operation. 636 | /// 637 | /// Should only be used for c interop functions. 638 | /// 639 | /// *Note*: this function could have unexpected results if your BEAM binary 640 | /// contains any zero byte values. Always use `get_char_slice/2` when 641 | /// C-interop is not necessary. 642 | /// 643 | pub fn get_c_string(environment: env, src_term: term) ![*c]u8 { 644 | var bin: binary = undefined; 645 | if (0 != e.enif_inspect_binary(environment, src_term, &bin)) { 646 | return bin.data; 647 | } else { 648 | return Error.@"Function clause error"; 649 | } 650 | } 651 | 652 | /// Takes an BEAM `t:binary/0` term and retrieves it as a Zig character slice 653 | /// (`[]u8`) No memory is allocated for this operation. 654 | /// 655 | pub fn get_char_slice(environment: env, src_term: term) ![]u8 { 656 | var bin: binary = undefined; 657 | 658 | if (0 != e.enif_inspect_binary(environment, src_term, &bin)) { 659 | return bin.data[0..bin.size]; 660 | } else { 661 | return Error.@"Function clause error"; 662 | } 663 | } 664 | 665 | /// Takes an BEAM `t:binary/0` term and returns the corresponding 666 | /// `binary` struct. 667 | /// 668 | pub fn get_binary(environment: env, src_term: term) !binary { 669 | var bin: binary = undefined; 670 | if (0 != e.enif_inspect_binary(environment, src_term, &bin)) { 671 | return bin; 672 | } else { 673 | return Error.@"Function clause error"; 674 | } 675 | } 676 | 677 | /////////////////////////////////////////////////////////////////////////////// 678 | // pids 679 | 680 | /// shorthand for `e.ErlNifPid`. 681 | pub const pid = e.ErlNifPid; 682 | 683 | /// Takes an BEAM `t:pid/0` term and returns the corresponding `pid` 684 | /// struct. 685 | /// 686 | /// Note that this is a fairly opaque struct and you're on your 687 | /// own as to what you can do with this (for now), except as a argument 688 | /// for the `e.enif_send` function. 689 | /// 690 | pub fn get_pid(environment: env, src_term: term) !pid { 691 | var result: pid = undefined; 692 | if (0 != e.enif_get_local_pid(environment, src_term, &result)) { 693 | return result; 694 | } else { 695 | return Error.@"Function clause error"; 696 | } 697 | } 698 | 699 | /// shortcut for `e.enif_self`, marshalling into zig error style. 700 | /// 701 | /// returns the pid value if it's env is a process-bound environment, otherwise 702 | /// running the wrapped function. That way, `beam.self()` is safe to use when 703 | /// you swap between different execution modes. 704 | /// 705 | /// if you need the process mailbox for the actual spawned thread, use `e.enif_self` 706 | pub fn self(environment: env) !pid { 707 | var p: pid = undefined; 708 | if (e.enif_self(environment, @ptrCast(&p))) |self_val| { 709 | return self_val.*; 710 | } else { 711 | return Error.@"Fail to get calling process"; 712 | } 713 | } 714 | 715 | /// shortcut for `e.enif_send` 716 | /// 717 | /// returns true if the send is successful, false otherwise. 718 | /// 719 | /// NOTE this function assumes a valid BEAM environment. If you have spawned 720 | /// an OS thread without a BEAM environment, you must use `send_advanced/4` 721 | pub fn send(c_env: env, to_pid: pid, msg: term) bool { 722 | return (e.enif_send(c_env, &to_pid, null, msg) == 1); 723 | } 724 | 725 | /// shortcut for `e.enif_send` 726 | /// 727 | /// returns true if the send is successful, false otherwise. 728 | /// 729 | /// if you are sending from a thread that does not have a BEAM environment, you 730 | /// should put `null` in both environment variables. 731 | pub fn send_advanced(c_env: env, to_pid: pid, m_env: env, msg: term) bool { 732 | return (e.enif_send(c_env, &to_pid, m_env, msg) == 1); 733 | } 734 | 735 | /////////////////////////////////////////////////////////////////////////////// 736 | // tuples 737 | 738 | /// Takes an Beam `t:tuple/0` term and returns it as a slice of `term` structs. 739 | /// Does *not* allocate memory for this operation. 740 | /// 741 | pub fn get_tuple(environment: env, src_term: term) ![]term { 742 | var length: c_int = 0; 743 | var term_list: [*c]term = null; 744 | if (0 != e.enif_get_tuple(environment, src_term, &length, &term_list)) { 745 | return term_list[0..(length - 1)]; 746 | } else { 747 | return Error.@"Function clause error"; 748 | } 749 | } 750 | 751 | /////////////////////////////////////////////////////////////////////////////// 752 | // lists 753 | 754 | /// Takes a BEAM `t:list/0` term and returns its length. 755 | /// 756 | pub fn get_list_length(environment: env, list: term) !usize { 757 | var result: c_uint = undefined; 758 | if (0 != e.enif_get_list_length(environment, list, &result)) { 759 | return @intCast(result); 760 | } else { 761 | return Error.@"Function clause error"; 762 | } 763 | } 764 | 765 | /// Iterates over a BEAM `t:list/0`. 766 | /// 767 | /// In this function, the `list` value will be modified to the `tl` of the 768 | /// BEAM list, and the return value will be the BEAM term. 769 | /// 770 | pub fn get_head_and_iter(environment: env, list: *term) !term { 771 | var head: term = undefined; 772 | if (0 != e.enif_get_list_cell(environment, list.*, &head, list)) { 773 | return head; 774 | } else { 775 | return Error.@"Function clause error"; 776 | } 777 | } 778 | 779 | /// A generic function which lets you convert a BEAM `t:list/0` of 780 | /// homogeous type into a Zig slice. 781 | /// 782 | /// The resulting slice will be allocated using the beam allocator, with 783 | /// ownership passed to the caller. If you need to use a different allocator, 784 | /// use `get_slice_of_alloc/4` 785 | /// 786 | /// supported internal types: 787 | /// - `c_int` 788 | /// - `c_long` 789 | /// - `isize` 790 | /// - `usize` 791 | /// - `u8` 792 | /// - `i32` 793 | /// - `i64` 794 | /// - `f16` 795 | /// - `f32` 796 | /// - `f64` 797 | pub fn get_slice_of(comptime T: type, environment: env, list: term) ![]T { 798 | return get_slice_of_alloc(T, allocator, environment, list); 799 | } 800 | 801 | /// Converts an BEAM `t:list/0` of homogeneous type into a Zig slice, but 802 | /// using any allocator you wish. 803 | /// 804 | /// ownership is passed to the caller. 805 | /// 806 | /// supported internal types: 807 | /// - `c_int` 808 | /// - `c_long` 809 | /// - `isize` 810 | /// - `usize` 811 | /// - `u8` 812 | /// - `i32` 813 | /// - `i64` 814 | /// - `f16` 815 | /// - `f32` 816 | /// - `f64` 817 | pub fn get_slice_of_alloc(comptime T: type, a: Allocator, environment: env, list: term) ![]T { 818 | const size = try get_list_length(environment, list); 819 | 820 | var idx: usize = 0; 821 | var head: term = undefined; 822 | 823 | // allocate memory for the Zig list. 824 | var result = try a.alloc(T, size); 825 | var movable_list = list; 826 | 827 | while (idx < size) { 828 | head = try get_head_and_iter(environment, &movable_list); 829 | result[idx] = try get(T, environment, head); 830 | idx += 1; 831 | } 832 | errdefer a.free(result); 833 | 834 | return result; 835 | } 836 | 837 | /////////////////////////////////////////////////////////////////////////////// 838 | // booleans 839 | 840 | /// private helper string comparison function 841 | fn str_cmp(comptime ref: []const u8, str: []const u8) bool { 842 | if (str.len != ref.len) { 843 | return false; 844 | } 845 | for (str, 0..) |item, idx| { 846 | if (item != ref[idx]) { 847 | return false; 848 | } 849 | } 850 | return true; 851 | } 852 | 853 | const true_slice = "true"[0..]; 854 | const false_slice = "false"[0..]; 855 | /// Converts an BEAM `t:boolean/0` into a Zig `bool`. 856 | /// 857 | /// May potentially raise an out of memory error, as it must make an allocation 858 | /// to perform its conversion. 859 | pub fn get_bool(environment: env, val: term) !bool { 860 | var str: []u8 = undefined; 861 | str = try get_atom_slice(environment, val); 862 | defer allocator.free(str); 863 | 864 | if (str_cmp(true_slice, str)) { 865 | return true; 866 | } else if (str_cmp(false_slice, str)) { 867 | return false; 868 | } else { 869 | return Error.@"Fail to get boolean"; 870 | } 871 | } 872 | 873 | /////////////////////////////////////////////////////////////////////////////// 874 | // syntactic sugar: makes 875 | /////////////////////////////////////////////////////////////////////////////// 876 | 877 | /////////////////////////////////////////////////////////////////////////////// 878 | // generic 879 | 880 | /// A helper for marshalling values from Zig back into the runtime. Use this 881 | /// function if you need support for Zig generics. 882 | /// 883 | /// supported types: 884 | /// - `c_int` 885 | /// - `c_long` 886 | /// - `isize` 887 | /// - `usize` 888 | /// - `u8` 889 | /// - `i32` 890 | /// - `i64` 891 | /// - `f16` 892 | /// - `f32` 893 | /// - `f64` 894 | pub fn make(comptime T: type, environment: env, val: T) !term { 895 | switch (T) { 896 | bool => return make_bool(environment, val), 897 | u8 => return make_u8(environment, val), 898 | u16 => return make_u16(environment, val), 899 | u32 => return make_u32(environment, val), 900 | u64 => return make_u64(environment, val), 901 | c_int => return make_c_int(environment, val), 902 | c_uint => return make_c_uint(environment, val), 903 | c_long => return make_c_long(environment, val), 904 | c_ulong => return make_c_ulong(environment, val), 905 | isize => return make_isize(environment, val), 906 | usize => return make_usize(environment, val), 907 | i8 => return make_i8(environment, val), 908 | i16 => return make_i16(environment, val), 909 | i32 => return make_i32(environment, val), 910 | i64 => return make_i64(environment, val), 911 | f16 => return make_f16(environment, val), 912 | f32 => return make_f32(environment, val), 913 | f64 => return make_f64(environment, val), 914 | ?*anyopaque => return make_u64(environment, @intFromPtr(val)), 915 | else => return Error.@"Function clause error", 916 | } 917 | } 918 | 919 | /// converts a char (`u8`) value into a BEAM `t:integer/0`. 920 | pub fn make_u8(environment: env, chr: u8) term { 921 | return e.enif_make_uint(environment, @intCast(chr)); 922 | } 923 | 924 | /// converts a unsigned (`u16`) value into a BEAM `t:integer/0`. 925 | pub fn make_u16(environment: env, val: u16) term { 926 | return e.enif_make_uint(environment, @intCast(val)); 927 | } 928 | 929 | /// converts a unsigned (`u32`) value into a BEAM `t:integer/0`. 930 | pub fn make_u32(environment: env, val: u32) term { 931 | return e.enif_make_uint(environment, @intCast(val)); 932 | } 933 | 934 | /// converts a unsigned (`u64`) value into a BEAM `t:integer/0`. 935 | pub fn make_u64(environment: env, val: u64) term { 936 | return e.enif_make_ulong(environment, @intCast(val)); 937 | } 938 | 939 | /// converts a `c_int` value into a BEAM `t:integer/0`. 940 | pub fn make_c_int(environment: env, val: c_int) term { 941 | return e.enif_make_int(environment, val); 942 | } 943 | 944 | /// converts a `c_uint` value into a BEAM `t:integer/0`. 945 | pub fn make_c_uint(environment: env, val: c_uint) term { 946 | return e.enif_make_uint(environment, val); 947 | } 948 | 949 | /// converts a `c_long` value into a BEAM `t:integer/0`. 950 | pub fn make_c_long(environment: env, val: c_long) term { 951 | return e.enif_make_long(environment, val); 952 | } 953 | 954 | /// converts a `c_ulong` value into a BEAM `t:integer/0`. 955 | pub fn make_c_ulong(environment: env, val: c_ulong) term { 956 | return e.enif_make_ulong(environment, val); 957 | } 958 | 959 | /// converts an `i8` value into a BEAM `t:integer/0`. 960 | pub fn make_i8(environment: env, val: i8) term { 961 | return e.enif_make_int(environment, @intCast(val)); 962 | } 963 | 964 | /// converts an `i16` value into a BEAM `t:integer/0`. 965 | pub fn make_i16(environment: env, val: i16) term { 966 | return e.enif_make_int(environment, @intCast(val)); 967 | } 968 | 969 | /// converts an `isize` value into a BEAM `t:integer/0`. 970 | pub fn make_isize(environment: env, val: isize) term { 971 | return e.enif_make_int(environment, @intCast(val)); 972 | } 973 | 974 | /// converts a `usize` value into a BEAM `t:integer/0`. 975 | pub fn make_usize(environment: env, val: usize) term { 976 | return e.enif_make_int(environment, @intCast(val)); 977 | } 978 | 979 | /// converts an `i32` value into a BEAM `t:integer/0`. 980 | pub fn make_i32(environment: env, val: i32) term { 981 | return e.enif_make_int(environment, @intCast(val)); 982 | } 983 | 984 | /// converts an `i64` value into a BEAM `t:integer/0`. 985 | pub fn make_i64(environment: env, val: i64) term { 986 | return e.enif_make_long(environment, @intCast(val)); 987 | } 988 | 989 | /////////////////////////////////////////////////////////////////////////////// 990 | // floats 991 | 992 | /// converts an `f16` value into a BEAM `t:float/0`. 993 | pub fn make_f16(environment: env, val: f16) term { 994 | return e.enif_make_double(environment, @floatCast(val)); 995 | } 996 | 997 | /// converts an `f32` value into a BEAM `t:float/0`. 998 | pub fn make_f32(environment: env, val: f32) term { 999 | return e.enif_make_double(environment, @floatCast(val)); 1000 | } 1001 | 1002 | /// converts an `f64` value into a BEAM `t:float/0`. 1003 | pub fn make_f64(environment: env, val: f64) term { 1004 | return e.enif_make_double(environment, val); 1005 | } 1006 | 1007 | /////////////////////////////////////////////////////////////////////////////// 1008 | // atoms 1009 | 1010 | /// converts a Zig char slice (`[]u8`) into a BEAM `t:atom/0`. 1011 | pub fn make_atom(environment: env, atom_str: []const u8) term { 1012 | return e.enif_make_atom_len(environment, @ptrCast(&atom_str[0]), atom_str.len); 1013 | } 1014 | 1015 | /////////////////////////////////////////////////////////////////////////////// 1016 | // binaries 1017 | 1018 | /// converts a Zig char slice (`[]u8`) into a BEAM `t:binary/0`. 1019 | /// 1020 | /// no memory allocation inside of Zig is performed and the BEAM environment 1021 | /// is responsible for the resulting binary. You are responsible for managing 1022 | /// the allocation of the slice. 1023 | pub fn make_slice(environment: env, val: []const u8) term { 1024 | var result: e.ERL_NIF_TERM = undefined; 1025 | 1026 | var bin: [*]u8 = @ptrCast(e.enif_make_new_binary(environment, val.len, &result)); 1027 | 1028 | for (val, 0..) |_, i| { 1029 | bin[i] = val[i]; 1030 | } 1031 | 1032 | return result; 1033 | } 1034 | 1035 | /// converts an c string (`[*c]u8`) into a BEAM `t:binary/0`. Mostly used for 1036 | /// c interop. 1037 | /// 1038 | /// no memory allocation inside of Zig is performed and the BEAM environment 1039 | /// is responsible for the resulting binary. You are responsible for managing 1040 | /// the allocation of the slice. 1041 | pub fn make_c_string(environment: env, val: [*c]const u8) term { 1042 | const result: e.ERL_NIF_TERM = undefined; 1043 | var len: usize = 0; 1044 | 1045 | // first get the length of the c string. 1046 | for (result, 0..) |chr, i| { 1047 | if (chr == 0) { 1048 | break; 1049 | } 1050 | len = i; 1051 | } 1052 | 1053 | // punt to the slicing function. 1054 | return make_slice(environment, val[0 .. len + 1]); 1055 | } 1056 | 1057 | /////////////////////////////////////////////////////////////////////////////// 1058 | // tuples 1059 | 1060 | /// converts a slice of `term`s into a BEAM `t:tuple/0`. 1061 | pub fn make_tuple(environment: env, val: []term) term { 1062 | return e.enif_make_tuple_from_array(environment, @ptrCast(val.ptr), @intCast(val.len)); 1063 | } 1064 | 1065 | /////////////////////////////////////////////////////////////////////////////// 1066 | // lists 1067 | 1068 | /// converts a slice of `term`s into a BEAM `t:list/0`. 1069 | pub fn make_term_list(environment: env, val: []term) term { 1070 | return e.enif_make_list_from_array(environment, @ptrCast(val.ptr), @intCast(val.len)); 1071 | } 1072 | 1073 | /// converts a Zig char slice (`[]u8`) into a BEAM `t:charlist/0`. 1074 | pub fn make_charlist(environment: env, val: []const u8) term { 1075 | return e.enif_make_string_len(environment, val, val.len, __latin1); 1076 | } 1077 | 1078 | /// converts a c string (`[*c]u8`) into a BEAM `t:charlist/0`. 1079 | pub fn make_c_string_charlist(environment: env, val: [*c]const u8) term { 1080 | return e.enif_make_string(environment, val, __latin1); 1081 | } 1082 | 1083 | pub fn make_charlist_len(environment: env, val: [*c]const u8, length: usize) term { 1084 | return e.enif_make_string_len(environment, val, length, __latin1); 1085 | } 1086 | 1087 | /////////////////////////////////////////////////////////////////////////////// 1088 | // list-generic 1089 | 1090 | /// A helper to make BEAM lists out of slices of `term`. Use this function if 1091 | /// you need a generic listbuilding function. 1092 | /// 1093 | /// uses the BEAM allocator internally. If you would like to use a custom 1094 | /// allocator, (for example an arena allocator, if you have very long lists), 1095 | /// use `make_list_alloc/4` 1096 | /// 1097 | /// supported internal types: 1098 | /// - `c_int` 1099 | /// - `c_long` 1100 | /// - `isize` 1101 | /// - `usize` 1102 | /// - `u8` 1103 | /// - `i32` 1104 | /// - `i64` 1105 | /// - `f16` 1106 | /// - `f32` 1107 | /// - `f64` 1108 | pub fn make_list(comptime T: type, environment: env, val: []T) !term { 1109 | return make_list_alloc(T, allocator, environment, val); 1110 | } 1111 | 1112 | /// A helper to make a BEAM `t:Kernel.list` out of `term`s, with any allocator. 1113 | /// Use this function if you need a generic listbuilding function. 1114 | /// 1115 | /// supported internal types: 1116 | /// - `c_int` 1117 | /// - `c_long` 1118 | /// - `isize` 1119 | /// - `usize` 1120 | /// - `u8` 1121 | /// - `i32` 1122 | /// - `i64` 1123 | /// - `f16` 1124 | /// - `f32` 1125 | /// - `f64` 1126 | pub fn make_list_alloc(comptime T: type, a: Allocator, environment: env, val: []T) !term { 1127 | var term_slice: []term = try a.alloc(term, val.len); 1128 | defer a.free(term_slice); 1129 | 1130 | for (val, 0..) |item, idx| { 1131 | term_slice[idx] = make(T, environment, item); 1132 | } 1133 | 1134 | return e.enif_make_list_from_array(environment, @ptrCast(&term_slice[0]), @intCast(val.len)); 1135 | } 1136 | 1137 | /// converts a c_int slice (`[]c_int`) into a BEAM list of `integer/0`. 1138 | pub fn make_c_int_list(environment: env, val: []c_int) !term { 1139 | return try make_list(c_int, environment, val); 1140 | } 1141 | 1142 | /// converts a c_long slice (`[]c_long`) into a BEAM list of `integer/0`. 1143 | pub fn make_c_long_list(environment: env, val: []c_long) !term { 1144 | return try make_list(c_long, environment, val); 1145 | } 1146 | 1147 | /// converts an i32 slice (`[]i32`) into a BEAM list of `integer/0`. 1148 | pub fn make_i32_list(environment: env, val: []i32) !term { 1149 | return try make_list(i32, environment, val); 1150 | } 1151 | 1152 | /// converts an i64 slice (`[]i64`) into a BEAM list of `integer/0`. 1153 | pub fn make_i64_list(environment: env, val: []i64) !term { 1154 | return try make_list(i64, environment, val); 1155 | } 1156 | 1157 | /// converts an f16 slice (`[]f16`) into a BEAM list of `t:float/0`. 1158 | pub fn make_f16_list(environment: env, val: []f16) !term { 1159 | return try make_list(f16, environment, val); 1160 | } 1161 | 1162 | /// converts an f32 slice (`[]f32`) into a BEAM list of `t:float/0`. 1163 | pub fn make_f32_list(environment: env, val: []f32) !term { 1164 | return try make_list(f32, environment, val); 1165 | } 1166 | 1167 | /// converts an f64 slice (`[]f64`) into a BEAM list of `t:float/0`. 1168 | pub fn make_f64_list(environment: env, val: []f64) !term { 1169 | return try make_list(f64, environment, val); 1170 | } 1171 | 1172 | /////////////////////////////////////////////////////////////////////////////// 1173 | // special atoms 1174 | 1175 | /// converts a `bool` value into a `t:boolean/0` value. 1176 | pub fn make_bool(environment: env, val: bool) term { 1177 | return if (val) e.enif_make_atom(environment, "true") else e.enif_make_atom(environment, "false"); 1178 | } 1179 | 1180 | /// creates a beam `nil` value. 1181 | pub fn make_nil(environment: env) term { 1182 | return e.enif_make_atom(environment, "nil"); 1183 | } 1184 | 1185 | /// creates a beam `ok` value. 1186 | pub fn make_ok(environment: env) term { 1187 | return e.enif_make_atom(environment, "ok"); 1188 | } 1189 | 1190 | /// creates a beam `error` value. 1191 | pub fn make_error(environment: env) term { 1192 | return e.enif_make_atom(environment, "error"); 1193 | } 1194 | 1195 | /////////////////////////////////////////////////////////////////////////////// 1196 | // ok and error tuples 1197 | 1198 | /// A helper to make `{:ok, term}` terms from arbitrarily-typed values. 1199 | /// 1200 | /// supported types: 1201 | /// - `c_int` 1202 | /// - `c_long` 1203 | /// - `isize` 1204 | /// - `usize` 1205 | /// - `u8` 1206 | /// - `i32` 1207 | /// - `i64` 1208 | /// - `f16` 1209 | /// - `f32` 1210 | /// - `f64` 1211 | /// 1212 | /// Use `make_ok_term/2` to make ok tuples from generic terms. 1213 | /// Use `make_ok_atom/2` to make ok tuples with atom terms from slices. 1214 | pub fn make_ok_tuple(comptime T: type, environment: env, val: T) term { 1215 | return make_ok_term(environment, make(T, environment, val)); 1216 | } 1217 | 1218 | /// A helper to make `{:ok, binary}` terms from slices 1219 | pub fn make_ok_binary(environment: env, val: []const u8) term { 1220 | return make_ok_term(environment, make_slice(environment, val)); 1221 | } 1222 | 1223 | /// A helper to make `{:ok, atom}` terms from slices 1224 | pub fn make_ok_atom(environment: env, val: []const u8) term { 1225 | return make_ok_term(environment, make_atom(environment, val)); 1226 | } 1227 | 1228 | /// A helper to make `{:ok, term}` terms in general 1229 | pub fn make_ok_term(environment: env, val: term) term { 1230 | return e.enif_make_tuple(environment, 2, make_ok(environment), val); 1231 | } 1232 | 1233 | /// A helper to make `{:error, term}` terms from arbitrarily-typed values. 1234 | /// 1235 | /// supported types: 1236 | /// - `c_int` 1237 | /// - `c_long` 1238 | /// - `isize` 1239 | /// - `usize` 1240 | /// - `u8` 1241 | /// - `i32` 1242 | /// - `i64` 1243 | /// - `f16` 1244 | /// - `f32` 1245 | /// - `f64` 1246 | /// 1247 | /// Use `make_error_term/2` to make error tuples from generic terms. 1248 | /// Use `make_error_atom/2` to make atom errors from slices. 1249 | pub fn make_error_tuple(comptime T: type, environment: env, val: T) term { 1250 | return make_error_term(environment, make(T, environment, val)); 1251 | } 1252 | 1253 | /// A helper to make `{:error, atom}` terms from slices 1254 | pub fn make_error_atom(environment: env, val: []const u8) term { 1255 | return make_error_term(environment, make_atom(environment, val)); 1256 | } 1257 | 1258 | /// A helper to make `{:error, binary}` terms from slices 1259 | pub fn make_error_binary(environment: env, val: []const u8) term { 1260 | return make_error_term(environment, make_slice(environment, val)); 1261 | } 1262 | 1263 | /// A helper to make `{:error, term}` terms in general 1264 | pub fn make_error_term(environment: env, val: term) term { 1265 | return e.enif_make_tuple(environment, 2, make_error(environment), val); 1266 | } 1267 | 1268 | /////////////////////////////////////////////////////////////////////////////// 1269 | // refs 1270 | 1271 | /// Encapsulates `e.enif_make_ref` 1272 | pub fn make_ref(environment: env) term { 1273 | return e.enif_make_ref(environment); 1274 | } 1275 | 1276 | /////////////////////////////////////////////////////////////////////////////// 1277 | // resources 1278 | 1279 | pub const resource_type = ?*e.ErlNifResourceType; 1280 | 1281 | /////////////////////////////////////////////////////////////////////////////// 1282 | // errors, etc. 1283 | 1284 | pub fn raise(environment: env, exception: term) term { 1285 | return e.enif_raise_exception(environment, exception); 1286 | } 1287 | 1288 | // create a global enomem string, then throw it. 1289 | const enomem_slice = "enomem"; 1290 | 1291 | /// This function is used to communicate `:enomem` back to the BEAM as an 1292 | /// exception. 1293 | /// 1294 | /// The BEAM is potentially OOM-safe, and Zig lets you leverage that. 1295 | /// OOM errors from `beam.allocator` can be converted to a generic erlang term 1296 | /// that represents an exception. Returning this from your NIF results in 1297 | /// a BEAM throw event. 1298 | pub fn raise_enomem(environment: env) term { 1299 | return e.enif_raise_exception(environment, make_atom(environment, enomem_slice)); 1300 | } 1301 | 1302 | const f_c_e_slice = "function_clause"; 1303 | 1304 | /// This function is used to communicate `:function_clause` back to the BEAM as an 1305 | /// exception. 1306 | /// 1307 | /// By default Zigler will do argument input checking on value 1308 | /// ingress from the dynamic BEAM runtime to the static Zig runtime. 1309 | /// You can also use this function to communicate a similar error by returning the 1310 | /// resulting term from your NIF. 1311 | pub fn raise_function_clause_error(env_: env) term { 1312 | return e.enif_raise_exception(env_, make_atom(env_, f_c_e_slice)); 1313 | } 1314 | 1315 | const resource_error = "resource_error"; 1316 | 1317 | /// This function is used to communicate `:resource_error` back to the BEAM as an 1318 | /// exception. 1319 | pub fn raise_resource_error(env_: env) term { 1320 | return e.enif_raise_exception(env_, make_atom(env_, resource_error)); 1321 | } 1322 | 1323 | const assert_slice = "assertion_error"; 1324 | 1325 | /// This function is used to communicate `:assertion_error` back to the BEAM as an 1326 | /// exception. 1327 | /// 1328 | /// Used when running Zigtests, when trapping `beam.AssertionError.AssertionError`. 1329 | pub fn raise_assertion_error(env_: env) term { 1330 | return e.enif_raise_exception(env_, make_atom(env_, assert_slice)); 1331 | } 1332 | 1333 | pub fn make_exception(env_: env, exception_module: []const u8, err: anyerror) term { 1334 | const erl_err = make_slice(env_, @errorName(err)); 1335 | var exception = e.enif_make_new_map(env_); 1336 | // define the struct 1337 | _ = e.enif_make_map_put(env_, exception, make_atom(env_, "__struct__"), make_atom(env_, exception_module), &exception); 1338 | _ = e.enif_make_map_put(env_, exception, make_atom(env_, "__exception__"), make_bool(env_, true), &exception); 1339 | // define the error 1340 | _ = e.enif_make_map_put(env_, exception, make_atom(env_, "message"), erl_err, &exception); 1341 | return exception; 1342 | } 1343 | 1344 | pub fn raise_exception(env_: env, exception_module: []const u8, err: anyerror) term { 1345 | return e.enif_raise_exception(env_, make_exception(env_, exception_module, err)); 1346 | } 1347 | 1348 | /// !value 1349 | /// you can use this value to access the BEAM environment of your unit test. 1350 | pub threadlocal var test_env: env = undefined; 1351 | 1352 | /////////////////////////////////////////////////////////////////////////////// 1353 | // NIF LOADING Boilerplate 1354 | 1355 | pub export fn blank_load(_: env, _: [*c]?*anyopaque, _: term) c_int { 1356 | return 0; 1357 | } 1358 | 1359 | pub export fn blank_upgrade(_: env, _: [*c]?*anyopaque, _: [*c]?*anyopaque, _: term) c_int { 1360 | return 0; 1361 | } 1362 | 1363 | const nil_slice = "nil"[0..]; 1364 | pub fn is_nil(environment: env, val: term) !bool { 1365 | var str: []u8 = undefined; 1366 | str = try get_atom_slice(environment, val); 1367 | defer allocator.free(str); 1368 | if (str_cmp(nil_slice, str)) { 1369 | return true; 1370 | } else { 1371 | return false; 1372 | } 1373 | } 1374 | 1375 | pub fn is_nil2(environment: env, val: term) bool { 1376 | return is_nil(environment, val) catch false; 1377 | } 1378 | 1379 | pub fn fetch_resource(comptime T: type, environment: env, res_typ: resource_type, res_trm: term) !T { 1380 | var obj: ?*anyopaque = null; 1381 | if (0 == e.enif_get_resource(environment, res_trm, res_typ, @ptrCast(&obj))) { 1382 | return try get(T, environment, res_trm); 1383 | } 1384 | if (obj != null) { 1385 | const val: *T = @ptrCast(@alignCast(obj)); 1386 | return val.*; 1387 | } else { 1388 | return Error.@"Fail to fetch resource"; 1389 | } 1390 | } 1391 | 1392 | pub fn fetch_resource_wrapped(comptime T: type, environment: env, arg: term) !T { 1393 | return fetch_resource(T, environment, T.resource_type, arg); 1394 | } 1395 | 1396 | pub fn fetch_ptr_resource_wrapped(comptime T: type, environment: env, arg: term) !*T { 1397 | return fetch_resource(*T, environment, T.resource_type, arg); 1398 | } 1399 | 1400 | pub fn fetch_resource_ptr(comptime PtrT: type, environment: env, res_typ: resource_type, res_trm: term) !PtrT { 1401 | var obj: PtrT = undefined; 1402 | if (0 == e.enif_get_resource(environment, res_trm, res_typ, @ptrCast(&obj))) { 1403 | return Error.@"Fail to fetch resource ptr"; 1404 | } 1405 | return obj; 1406 | } 1407 | 1408 | // res_typ should be opened resource type of the array resource 1409 | pub fn get_resource_array_from_list(comptime ElementType: type, environment: env, resource_type_element: resource_type, resource_type_array: resource_type, list: term) !term { 1410 | const size = try get_list_length(environment, list); 1411 | 1412 | const U8Ptr = [*c]u8; 1413 | switch (@typeInfo(ElementType)) { 1414 | .@"struct" => |s| { 1415 | if (s.layout != .@"extern") { 1416 | return Error.@"Fail to fetch resource list element"; 1417 | } 1418 | }, 1419 | else => {}, 1420 | } 1421 | const ArrayPtr = [*c]ElementType; 1422 | const ptr: ?*anyopaque = e.enif_alloc_resource(resource_type_array, @sizeOf(ArrayPtr) + size * @sizeOf(ElementType)); 1423 | var data_ptr: ArrayPtr = undefined; 1424 | if (ptr == null) { 1425 | unreachable(); 1426 | } else { 1427 | var obj: *ArrayPtr = undefined; 1428 | obj = @ptrCast(@alignCast(ptr)); 1429 | data_ptr = @ptrCast(@alignCast(@as(U8Ptr, @ptrCast(ptr)) + @sizeOf(ArrayPtr))); 1430 | if (size > 0) { 1431 | obj.* = data_ptr; 1432 | } else { 1433 | obj.* = 0; 1434 | } 1435 | } 1436 | var idx: usize = 0; 1437 | var head: term = undefined; 1438 | var movable_list = list; 1439 | 1440 | while (idx < size) { 1441 | head = try get_head_and_iter(environment, &movable_list); 1442 | if (fetch_resource(ElementType, environment, resource_type_element, head)) |value| { 1443 | data_ptr[idx] = value; 1444 | } else |_| { 1445 | return Error.@"Fail to fetch resource list element"; 1446 | } 1447 | idx += 1; 1448 | } 1449 | return e.enif_make_resource(environment, ptr); 1450 | } 1451 | 1452 | const mem = @import("std").mem; 1453 | 1454 | pub fn get_resource_array_from_binary(environment: env, resource_type_array: resource_type, binary_term: term) !term { 1455 | const RType = [*c]u8; 1456 | var bin: binary = undefined; 1457 | if (0 == e.enif_inspect_binary(environment, binary_term, &bin)) { 1458 | return Error.@"Fail to inspect resource binary"; 1459 | } 1460 | const ptr: ?*anyopaque = e.enif_alloc_resource(resource_type_array, @sizeOf(RType) + bin.size); 1461 | var obj: *RType = undefined; 1462 | var real_binary: RType = undefined; 1463 | if (ptr == null) { 1464 | unreachable(); 1465 | } else { 1466 | obj = @ptrCast(@alignCast(ptr)); 1467 | real_binary = @ptrCast(@alignCast(ptr)); 1468 | real_binary += @sizeOf(RType); 1469 | obj.* = real_binary; 1470 | } 1471 | mem.copyForwards(u8, real_binary[0..bin.size], bin.data[0..bin.size]); 1472 | return e.enif_make_resource(environment, ptr); 1473 | } 1474 | 1475 | // the term could be: 1476 | // - list of element resource 1477 | // - list of primitives 1478 | // - binary 1479 | pub fn get_resource_array(comptime ElementType: type, environment: env, resource_type_element: resource_type, resource_type_array: resource_type, data: term) !term { 1480 | if (get_resource_array_from_list(ElementType, environment, resource_type_element, resource_type_array, data)) |value| { 1481 | return value; 1482 | } else |_| { 1483 | if (get_resource_array_from_binary(environment, resource_type_array, data)) |value| { 1484 | return value; 1485 | } else |_| { 1486 | return Error.@"Function clause error"; 1487 | } 1488 | } 1489 | } 1490 | pub fn get_resource_ptr_from_term(environment: env, comptime PtrType: type, element_resource_type: resource_type, ptr_resource_type: resource_type, element: term) !term { 1491 | const ptr: ?*anyopaque = e.enif_alloc_resource(ptr_resource_type, @sizeOf(PtrType)); 1492 | var obj: *PtrType = undefined; 1493 | obj = @ptrCast(@alignCast(ptr)); 1494 | if (ptr == null) { 1495 | unreachable(); 1496 | } else { 1497 | obj.* = try fetch_resource_ptr(PtrType, environment, element_resource_type, element); 1498 | } 1499 | return e.enif_make_resource(environment, ptr); 1500 | } 1501 | 1502 | pub fn make_resource(environment: env, value: anytype, rst: resource_type) !term { 1503 | const RType = @TypeOf(value); 1504 | const ptr: ?*anyopaque = e.enif_alloc_resource(rst, @sizeOf(RType)); 1505 | var obj: *RType = undefined; 1506 | if (ptr == null) { 1507 | return Error.@"Fail to make resource"; 1508 | } else { 1509 | obj = @ptrCast(@alignCast(ptr)); 1510 | obj.* = value; 1511 | } 1512 | return e.enif_make_resource(environment, ptr); 1513 | } 1514 | 1515 | pub fn make_resource_wrapped(environment: env, value: anytype) !term { 1516 | return make_resource(environment, value, @TypeOf(value).resource_type); 1517 | } 1518 | 1519 | pub fn make_ptr_resource_wrapped(environment: env, ptr: anytype) !term { 1520 | return make_resource(environment, ptr, @TypeOf(ptr.*).resource_type); 1521 | } 1522 | 1523 | pub export fn destroy_do_nothing(_: env, _: ?*anyopaque) void {} 1524 | pub fn open_resource_wrapped(environment: env, comptime T: type) void { 1525 | T.resource_type = e.enif_open_resource_type(environment, null, T.resource_name, destroy_do_nothing, e.ERL_NIF_RT_CREATE | e.ERL_NIF_RT_TAKEOVER, null); 1526 | } 1527 | --------------------------------------------------------------------------------