sol2 icon indicating copy to clipboard operation
sol2 copied to clipboard

How to pass sol usertype by value inside Lua?

Open niello opened this issue 3 years ago • 5 comments

Hi.

This is not a bug report but a question. Sorry if it is already answered somewhere. I didn't manage to find anything relevant in docs or issues.

I have a couple of user types registered through sol, like this:

_Session->GetScriptState().new_usertype<DEM::Game::HEntity>("HEntity"
	, sol::constructors<sol::types<>, sol::types<const DEM::Game::HEntity&>>()
	, sol::meta_function::to_string, &DEM::Game::EntityToString
	, sol::meta_function::concatenation, sol::overload(
		[](const char* a, DEM::Game::HEntity b) { return a + DEM::Game::EntityToString(b); }
		, [](DEM::Game::HEntity a, const char* b) { return DEM::Game::EntityToString(a) + b; })
	);

What is important about these types is that they are pointer sized or less and can be seen as opaque integer handles.

The problem arises when I want to catch a value of this type into a Lua closure, e.g.:

-- EntityID is a var of type HEntity, passed as an argument from C++, where it is stored on stack
OpenUI(function()
	local SO = Session.World.SmartObject(EntityID)
	if SO then
		...
	end
end)

EntityID in this example is stored by reference, and is deleted from C++ stack short after calling OpenUI because it goes out of scope. And when a closure is called, EntityID inside Lua is invalid.

An ugly workaround is possible but it requires constant attention from the programmer and it is also slower because of the copy construction:

EntityIDCopy = HEntity.new(EntityID)
OpenUI(function()
	local SO = Session.World.SmartObject(EntityIDCopy)
	if SO then
		...
	end
end)

What I expect from HEntity usertype is that it is passed by value everywhere. This should not be a problem technically because it is just a number wrapped into a convenience class, and it is trivially copyable. Then it will be stored by value in a closure too, remaining valid forever. It is also faster than allocating a copy of HEntity on the heap when I pass it from C++ to Lua.

How should I setup sol usertype to achieve this? AFAIR raw Lua's "light userdata" offered something like this.

niello avatar Apr 01 '22 19:04 niello

AFAIR raw Lua's "light userdata" offered something like this

Maybe so, but if using lightuserdata you will not be able to have a metatable attached to the lightuserdata that contains methods. (my_light_userdata:Method())

EntityID in this example is stored by reference, and is deleted from C++ stack short after calling OpenUI because it goes out of scope. And when a closure is called, EntityID inside Lua is invalid.

Sounds like you are explicitly passing a pointer or an std::ref to sol even if you dont want to. If I remember correctly, sol should just make a copy every time you put something into lua if you pass it by a normal reference. Example: https://godbolt.org/z/9c9TWqese Would it be possible to just not pass a pointer/std::ref to sol?

Rochet2 avatar Apr 01 '22 21:04 Rochet2

No, I definitely pass it not by pointer or explicit std::ref.

C++ side:

static void CallTransitionScript(sol::function& Script, HEntity EntityID, CStrID CurrState, CStrID NextState)
{
	if (Script.valid())
	{
		auto Result = Script(EntityID, CurrState, NextState);
		if (!Result.valid())
		{
			sol::error Error = Result;
			::Sys::Error(Error.what());
		}
	}
}

...

CallTransitionScript(pSOAsset->GetScriptFunction(Lua, "OnTransitionEnd"), EntityID, SO.CurrState, SO.NextState);

Lua side:

function OnTransitionEnd(EntityID, PrevState, NewState)
	ContainerID = HEntity.new(EntityID) -- FIXME: have to make a copy, because EntityID is destroyed in C++
	OpenUI(EntityID, function()
		local SO = Session.World.SmartObject(ContainerID)
		...
	end)
end

Note that EntityID is passed by value. sol::function instantiates a call operator with HEntity& but this decision doesn't come from user code.

niello avatar Apr 02 '22 09:04 niello

Seems that passing by reference is done for functions. Function documentation describes this and how you can avoid it: https://sol2.readthedocs.io/en/latest/functions.html#functions-and-argument-passing

However, the way to avoid it was added for 4.0.0, which seems to be in alpha https://github.com/ThePhD/sol2/commit/561c90abf4e2106377cc1e29021ef60b4d6b9240

See SOL_FUNCTION_CALL_VALUE_SEMANTICS and is_value_semantic_for_function.

Rochet2 avatar Apr 02 '22 11:04 Rochet2

@niello by default sol::function passes lvalue reference arguments to lua by reference. Try to move arguments:

auto Result = Script(std::move(EntityID), std::move(CurrState), std::move(NextState));

Smertig avatar Apr 02 '22 11:04 Smertig

@Rochet2 , thank you, this looks like exactly what I need. I will consider to move to 4.0 when it is released. @Smertig , thank you too. I will try this out of curiosity, but I don't want to move values because it is a per-call solution (which means you can easily forget to do it at some place) and because I want to continue using EntityID after passing its value to Lua.

niello avatar Apr 02 '22 12:04 niello

is_value_semantic_for_function works in 3.3.0

niello avatar Jan 04 '23 12:01 niello