Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 87 additions & 4 deletions src/glua.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,92 @@ fn wrap_function(
fun: fn(Lua, List(dynamic.Dynamic)) -> #(Lua, List(Value)),
) -> Value

/// Allocates memory in the Lua state for an encoded value.
///
/// Whenever you try to set an encoded value in the Lua state or pass it as an argument to a Lua function,
/// `glua` allocates memory in the Lua state for that value automatically. This means that the same value
/// can be allocated multiple times depending on how you use it and that multiple, distinct versions
/// of that value could exist at runtime.
///
/// Consider this example:
///
/// ```gleam
/// let proxy =
/// fn(state, _) {
/// let assert Ok(#(state, _)) =
/// glua.ref_call_function_by_name(state:, keys: ["error"], args: [
/// glua.string("attempt to update a read-only table"),
/// ])
///
/// #(state, [])
/// }
/// |> glua.function
///
/// let state = glua.new()
/// let table = glua.table([])
/// let metatable = glua.table([#(glua.string("__newindex"), proxy)])
/// let assert Ok(#(state, _)) =
/// glua.ref_call_function_by_name(state:, keys: ["setmetatable"], args: [
/// table,
/// metatable,
/// ])
/// let assert Ok(state) = glua.set(state:, keys: ["my_table"], value: table)
///
/// let code =
/// "my_table.my_key = 'this should not be the value'; return my_table.my_key"
///
/// glua.eval(state:, code:, using: decode.string)
/// // -> Ok(#(_, ["this should not be the key"]))
/// ```
///
/// Here we expect that a Lua exception will be raised whenever we try to set a new key in `table`,
/// but we can still set new keys as usual. This happens because `table` is allocated twice, making the table
/// you pass as an argument to `setmetatable` and the table you set at `my_table` two different tables at runtime.
///
/// There are situations where you want to allocate memory only once for a value and make sure that
/// at runtime there is only one copy of that value. This function does exactly that.
///
/// We can fix our example by using `glua.alloc`:
///
/// ```gleam
/// let proxy =
/// fn(state, _) {
/// let assert Ok(#(state, _)) =
/// glua.ref_call_function_by_name(state:, keys: ["error"], args: [
/// glua.string("attempt to update a read-only table"),
/// ])
///
/// #(state, [])
/// }
/// |> glua.function
///
/// // we use `glua.alloc` here to avoid `table` to be allocated multiple times
/// let #(state, table) = glua.alloc(glua.new(), glua.table([]))
///
/// // we can keep `metatable` as it because it is only used once
/// let metatable = glua.table([#(glua.string("__newindex"), proxy)])
///
/// let assert Ok(#(state, _)) = glua.ref_call_function_by_name(
/// state:,
/// keys: ["setmetatable"],
/// args: [table, metatable]
/// )
/// let assert Ok(state) = glua.set(state:, keys: ["my_table"], value: table)
///
/// let code =
/// "my_table.my_key = 'this should not be the value'; return my_table.my_key"
///
/// glua.eval(state:, code:, using: decode.string)
/// // -> Error(glua.LuaRuntimeException(glua.ErrorCall([
/// "attempt to update a read-only table"
/// ]), _)
/// ```
///
/// > **Note**: This function should only be used to allocate memory for tables or userdata values.
/// > For the rest of types that can be encoded, like strings, integers, floats or booleans, this function does nothing.
@external(erlang, "glua_ffi", "alloc")
pub fn alloc(lua: Lua, v: Value) -> #(Lua, Value)

/// Creates a new Lua VM instance
@external(erlang, "luerl", "init")
pub fn new() -> Lua
Expand Down Expand Up @@ -333,7 +419,7 @@ pub fn set(
Ok(_) -> Ok(#(keys, lua))

Error(KeyNotFound) -> {
let #(tbl, lua) = alloc_table([], lua)
let #(lua, tbl) = alloc(lua, table([]))
do_set(lua, keys, tbl)
|> result.map(fn(lua) { #(keys, lua) })
}
Expand Down Expand Up @@ -400,9 +486,6 @@ pub fn set_lua_paths(
set(lua, ["package", "path"], paths)
}

@external(erlang, "luerl_emul", "alloc_table")
fn alloc_table(content: List(a), lua: Lua) -> #(a, Lua)

@external(erlang, "glua_ffi", "get_table_keys_dec")
fn do_get(lua: Lua, keys: List(String)) -> Result(dynamic.Dynamic, LuaError)

Expand Down
29 changes: 26 additions & 3 deletions src/glua_ffi.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

-import(luerl_lib, [lua_error/2]).

-export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2,
-export([coerce/1, coerce_nil/0, coerce_userdata/1, alloc/2, wrap_fun/1, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2,
get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2,
eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3]).

Expand Down Expand Up @@ -58,6 +58,20 @@ is_encoded({erl_mfa,_,_,_}) ->
is_encoded(_) ->
false.

encode(X, St0) ->
case is_encoded(X) of
true -> {X, St0};
false -> luerl:encode(X, St0)
end.

encode_list(L, St0) when is_list(L) ->
Enc = fun(X, {L1, St}) ->
{Enc, St1} = encode(X, St),
{[Enc | L1], St1}
end,
{L1, St1} = lists:foldl(Enc, {[], St0}, L),
{lists:reverse(L1), St1}.

%% TODO: Improve compiler errors handling and try to detect more errors
map_error({error, [{_, luerl_parse, Errors} | _], _}) ->
FormattedErrors = lists:map(fun(E) -> list_to_binary(E) end, Errors),
Expand Down Expand Up @@ -97,6 +111,15 @@ coerce_nil() ->
coerce_userdata(X) ->
{userdata, X}.

alloc(St0, Value) when is_list(Value) ->
{Enc, St1} = luerl_heap:alloc_table(Value, St0),
{St1, Enc};
alloc(St0, {usrdef,_}=Value) ->
{Enc, St1} = luerl_heap:alloc_userdata(Value, St0),
{St1, Enc};
alloc(St0, Other) ->
{St0, Other}.

wrap_fun(Fun) ->
fun(Args, State) ->
Decoded = luerl:decode_list(Args, State),
Expand Down Expand Up @@ -165,11 +188,11 @@ eval_file_dec(Lua, Path) ->
unicode:characters_to_list(Path), Lua)).

call_function(Lua, Fun, Args) ->
{EncodedArgs, State} = luerl:encode_list(Args, Lua),
{EncodedArgs, State} = encode_list(Args, Lua),
to_gleam(luerl:call(Fun, EncodedArgs, State)).

call_function_dec(Lua, Fun, Args) ->
{EncodedArgs, St1} = luerl:encode_list(Args, Lua),
{EncodedArgs, St1} = encode_list(Args, Lua),
case luerl:call(Fun, EncodedArgs, St1) of
{ok, Ret, St2} ->
Values = luerl:decode_list(Ret, St2),
Expand Down
19 changes: 19 additions & 0 deletions test/glua_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -551,3 +551,22 @@ pub fn nested_function_references_test() {
glua.call_function(state: lua, ref:, args: [arg], using: decode.float)
assert result == 20.0
}

pub fn alloc_test() {
let #(lua, table) = glua.alloc(glua.new(), glua.table([]))
let proxy =
glua.function(fn(lua, _args) { #(lua, [glua.string("constant")]) })
let metatable = glua.table([#(glua.string("__index"), proxy)])
let assert Ok(#(lua, _)) =
glua.ref_call_function_by_name(lua, ["setmetatable"], [table, metatable])
let assert Ok(lua) = glua.set(lua, ["test_table"], table)

let assert Ok(#(_lua, [ret1])) =
glua.eval(lua, "return test_table.any_key", decode.string)

let assert Ok(#(_lua, [ret2])) =
glua.eval(lua, "return test_table.other_key", decode.string)

assert ret1 == "constant"
assert ret2 == "constant"
}