Skip to content

Commit

Permalink
Merge pull request #413 from surferlocal/dynamic-load_data
Browse files Browse the repository at this point in the history
Add dynamic load data
  • Loading branch information
evnu committed Apr 19, 2023
2 parents f1aaa3b + f40cfec commit cb8c1e7
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,4 @@ jobs:
- name: Test mix project with example created from template
working-directory: rustler_mix
run: ./test.sh
if: "startsWith(matrix.os, 'ubuntu') && matrix.pair.latest"
if: "startsWith(matrix.os, 'ubuntu') && matrix.pair.latest"
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"rustler_tests/native/rustler_test",
"rustler_tests/native/rustler_bigint_test",
"rustler_tests/native/deprecated_macros",
"rustler_tests/native/dynamic_load",
"rustler_tests/native/rustler_compile_tests",
"rustler_benchmarks/native/benchmark",
]
50 changes: 49 additions & 1 deletion rustler_mix/lib/rustler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@ defmodule Rustler do
* `:load_data` - Any valid term. This value is passed into the NIF when it is
loaded (default: `0`)
* `:load_data_fun` - `{Module, :function}` to dynamically generate `load_data`.
Default value: `nil`.
This parameter is mutually exclusive with `load_data`
which means that `load_data` has to be set to it's default value.
Example
defmodule NIF do
use Rustler, load_data_fun: {Deployment, :nif_data}
end
defmodule Deployment do
def nif_data do
:code.priv_dir(:otp_app) |> IO.iodata_to_binary()
end
end
* `:load_from` - This option allows control over where the final artifact should be
loaded from at runtime. By default the compiled artifact is loaded from the
owning `:otp_app`'s `priv/native` directory. This option comes in handy in
Expand Down Expand Up @@ -86,6 +104,7 @@ defmodule Rustler do
if config.lib do
@load_from config.load_from
@load_data config.load_data
@load_data_fun config.load_data_fun

@before_compile Rustler
end
Expand All @@ -109,11 +128,40 @@ defmodule Rustler do
|> Application.app_dir(path)
|> to_charlist()

:erlang.load_nif(load_path, @load_data)
load_data = Rustler.construct_load_data(@load_data, @load_data_fun)

:erlang.load_nif(load_path, load_data)
end
end
end

def construct_load_data(load_data, load_data_fun) do
default_load_data_value = %Rustler.Compiler.Config{}.load_data
default_fun_value = %Rustler.Compiler.Config{}.load_data_fun

case {load_data, load_data_fun} do
{load_data, ^default_fun_value} ->
load_data

{^default_load_data_value, {module, function}}
when is_atom(module) and is_atom(function) ->
apply(module, function, [])

{^default_load_data_value, provided_value} ->
raise """
`load_data` has to be `{Module, :function}`.
Instead received: #{inspect(provided_value)}
"""

{load_data, load_data_fun} ->
raise """
Only `load_data` or `load_data_fun` can be provided. Instead received:
>>> load_data: #{inspect(load_data)}
>>> load_data_fun: #{inspect(load_data_fun)}
"""
end
end

@doc false
def rustler_version, do: "0.27.0"

Expand Down
1 change: 1 addition & 0 deletions rustler_mix/lib/rustler/compiler/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ defmodule Rustler.Compiler.Config do
features: [],
lib: true,
load_data: 0,
load_data_fun: nil,
load_from: nil,
mode: :release,
otp_app: nil,
Expand Down
14 changes: 14 additions & 0 deletions rustler_tests/lib/dynamic_data.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule DynamicData.Config do
def nif_data do
%{priv_path: :code.priv_dir(:rustler_test) |> IO.iodata_to_binary()}
end
end

defmodule DynamicData do
use Rustler,
otp_app: :rustler_test,
crate: :dynamic_load,
load_data_fun: {DynamicData.Config, :nif_data}

def get_dataset, do: :erlang.nif_error(:nif_not_loaded)
end
13 changes: 13 additions & 0 deletions rustler_tests/native/dynamic_load/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "dynamic_load"
version = "0.1.0"
edition = "2021"

[lib]
name = "dynamic_load"
path = "src/lib.rs"
crate-type = ["cdylib"]

[dependencies]
rustler_bigint = { path = "../../../rustler_bigint" }
rustler = { path = "../../../rustler" }
55 changes: 55 additions & 0 deletions rustler_tests/native/dynamic_load/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use rustler::Atom;
use std::{ffi::OsStr, fs::read_to_string, path::PathBuf};

static mut DATASET: Option<Box<str>> = None;

fn initialize_dataset(mut asset_path: PathBuf) {
asset_path.push("demo_dataset.txt");

// https://github.com/elixir-lsp/elixir-ls/issues/604
eprintln!("Loading dataset from {:?}.", &asset_path);

let data = read_to_string(asset_path).unwrap().into_boxed_str();
let data = Some(data);

// Safety: assumes that this function is being called once when
// dynamically loading this library.
// `load()` is being called exactly once and OTP will not allow any other function call
// before this function returns
unsafe { DATASET = data };
}

#[rustler::nif]
fn get_dataset() -> &'static str {
// Safety: see `initialize_dataset()`
unsafe { DATASET.as_ref() }.expect("Dataset is not initialized")
}

fn load<'a>(env: rustler::Env<'a>, args: rustler::Term<'a>) -> bool {
let key = Atom::from_str(env, "priv_path").unwrap().to_term(env);
let priv_path = args.map_get(key).unwrap();
let priv_path = priv_path.into_binary().unwrap().as_slice();

let asset_path = build_path_buf(priv_path);

initialize_dataset(asset_path);

true
}

#[cfg(unix)]
fn build_path_buf(priv_path: &[u8]) -> PathBuf {
use std::os::unix::prelude::OsStrExt;

let priv_path = OsStr::from_bytes(priv_path);
PathBuf::from(priv_path)
}

#[cfg(windows)]
fn build_path_buf(priv_path: &[u8]) -> PathBuf {
let string_slice = std::str::from_utf8(priv_path).expect("Data is not valid UTF-8, we could decode it without valid UTF-8 requirements but lets not do that for now because its easier this way");
let priv_path = OsStr::new(string_slice);
PathBuf::from(priv_path)
}

rustler::init!("Elixir.DynamicData", [get_dataset], load = load);
Empty file removed rustler_tests/priv/.gitkeep
Empty file.
1 change: 1 addition & 0 deletions rustler_tests/priv/demo_dataset.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some random dataset
10 changes: 10 additions & 0 deletions rustler_tests/test/dynamic_data_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule RustlerTest.DynamicDataTest do
use ExUnit.Case, async: true

test "rust has access to demo_dataset.txt via dynamic priv path" do
%{priv_path: path} = DynamicData.Config.nif_data()
path = Path.join(path, "demo_dataset.txt")

assert File.read!(path) == DynamicData.get_dataset()
end
end

0 comments on commit cb8c1e7

Please sign in to comment.