diff --git a/src/glua.gleam b/src/glua.gleam index cc5ae51..6c875c5 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -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 @@ -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) }) } @@ -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) diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 1edf9e7..d9cd476 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -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]). @@ -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), @@ -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), @@ -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), diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 184baa2..1232c28 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -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" +}