Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to have safe pointer walking/chaining ? #1491

Closed
UE4SS opened this issue May 11, 2023 · 2 comments
Closed

Is it possible to have safe pointer walking/chaining ? #1491

UE4SS opened this issue May 11, 2023 · 2 comments

Comments

@UE4SS
Copy link

UE4SS commented May 11, 2023

I have an existing system that I'd like to convert into sol.
Let's say I have a class, and this class has a linked list:

class Object
{
private:
  Object* Next;

public:
  Object* GetNext() { return Next; }
};

I'm using a linked list here just as an example but we use this same idea for accessing all types of pointers.
In our code, we do a lot of pointer walking/chaining so we created a useful shortcut to avoid large amounts of validity checking.
It means that this code:

local obj = GetObj()
if not obj then return end
local other_obj = obj:GetNext()
if not other_obj then return end
local other_other_obj = other_obj:GetNext()
if not other_other_obj then return end
local other_other_other_obj = other_other_obj:GetNext()
if not other_other_other_obj then return end
-- Now we know all pointers retrieved above are safe.

becomes:

local obj = GetObj():GetNext():GetNext():GetNext()
if not obj:IsValid() then return end
-- Now we know all pointers retrieved above are safe.

We could go through and manually create wrapper classes for all our classes but the only way I can think of to do that would be to also create wrappers for every member variable and member function that return Wrapper<OriginalType> instead of the original type directly and I think the complexity and amount of work might get out of hand fairly quickly.

So my question is: Does sol have any functionality built-in for this type of pointer walking/chaining or something to make it easier ?

@Rochet2
Copy link

Rochet2 commented May 11, 2023

So my question is: Does sol have any functionality built-in for this type of pointer walking/chaining

I have not seen a feature like that.

or something to make it easier ?

Closest that comes to mind are policies https://sol2.readthedocs.io/en/latest/api/policies.html that do things during a function call from what I gather.
But they are not really doing what you want. They could maybe be used as a part of the implementation though.

We could go through and manually create wrapper classes for all our classes but the only way I can think of to do that would be to also create wrappers for every member variable and member function that return Wrapper<OriginalType> instead of the original type directly

Maybe you could sketch this out a bit. I dont quite understand how you would intend to use the wrapper classes. Or what is the issue with the wrappers.

Also, how do you use the chaining in C++ currently? You cant do this in C++ GetObj():GetNext():GetNext():GetNext() with the example Object class since it would crash on nullptr.

Here are some ideas, I think each one is a bit different and has pros and cons. In the end they are very similar though:

(1) You can have a builder pattern system, where you have a lua implemented builder that holds "next" variable and advances it by calling GetNext if "next" is not null. Then it finally returns the "next" value at the end.
It does not matter if GetNext returns a value or nullptr.

function Chain(o)
    return {
        next = o,
        GetNext = function(self)
            if next then next = next:GetNext() end
            return self
        end,
        Get = function(self)
            return next
        end,
    }
end

local maybeExists = Chain(GetObj()):GetNext():GetNext():GetNext():Get()
if not maybeExists then return end

(2) You could use inheritance to make a dummy return object instead of nullptr.

struct LuaChainable { };

class Object : LuaChainable
{
private:
  Object* Next = nullptr;
public:
  Object* GetNext() { return Next ? Next : new LuaChainable(); } // probably memory leak. You want to return a pointer to a static instance instead
};

// bindings
  lua.new_usertype<LuaChainable>("LuaChainable",
    "GetNext", [](LuaChainable& self){ return self; },
    "IsValid", [](){ return false; }
  );
  lua.new_usertype<Object>("Object",
    sol::base_classes, sol::bases<LuaChainable>(),
    "GetNext", &Object::GetNext,
    "IsValid", [](){ return true; }
  );
local obj = GetObj():GetNext():GetNext():GetNext()
if not obj:IsValid() then return end

(3) I think you had some kind of wrapper method in mind

@UE4SS
Copy link
Author

UE4SS commented May 12, 2023

It seems that sol:policies will do the trick nicely.
This code will make any function with pointer_policy return an instance of InvalidObject instead of nil when the return value is nullptr or the instance is another InvalidObject.

struct InvalidObject
{
    static auto Index(sol::stack_object, sol::this_state) -> std::function<InvalidObject()>
    {
        return [] { return InvalidObject{}; };
    }
};

static auto pointer_policy(lua_State* lua_state, int current_stack_return_count) -> int
{
    // Multiple return values are not handled!
    if (current_stack_return_count == 1 && lua_isnil(lua_state, -1))
    {
        sol::stack::push(lua_state, InvalidObject{});   
    }
    return current_stack_return_count;
}

auto SolMod::start_mod() -> void
{
    // ...
    auto sol_invalid_object_class = sol().new_usertype<InvalidObject>(
        sol::meta_function::index, &InvalidObject::Index,
        "IsValid", [] { return false; }
    );
    // We have other criteria than 'nullptr' that determines if an object is valid.
    // Therefore, the 'IsValid' function is a non-lambda function that's not included in this example.
    // But this would otherwise just be '[] { return true; }'.
    auto sol_object_class = sol().new_usertype<Object>(
        "IsValid", &Object_IsValid,
        "GetNext", sol_policies(&Object::GetNext, &pointer_policy)
    );
}

I'm unwilling to modify the classes that I'm creating bindings for, therefore any changes to the class itself would require a wrapper struct and this is how we currently do it, more or less:

// Original class
class Object
{
    auto my_func() -> SomeType*;
};

// Wrapper class that Lua constructs with all sorts of userdata, values and metamethods.
// An instance of this is always constructed and then we can check if the 'internal' pointer is nullptr on usage.
// This allows for pointer chaining as long as the user remembers to call 'IsValid' before any other function.
class LuaObject
{
    Object* internal{};
};

So my unwillingness to change the original class and my desire to not have to create wrappers rules out your second example.
Your first example is interesting and would've been a good compromise if sol::policies hadn't worked.

Thanks for your help.

@UE4SS UE4SS closed this as completed May 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants