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

Extend bound classes with custom methods in Lua #114

Closed
dtroitskiy opened this issue Apr 9, 2023 · 66 comments · Fixed by #116 or #136
Closed

Extend bound classes with custom methods in Lua #114

dtroitskiy opened this issue Apr 9, 2023 · 66 comments · Fixed by #116 or #136
Labels
enhancement New feature or request

Comments

@dtroitskiy
Copy link

In our project we need to extend classes bound from C++ with custom methods in Lua.
It may look like the following:

// C++
getGlobalNamespace(luaState)
  .beginClass<Creature>("Creature")
  // Property and method bindings go here ...
-- Lua
function Creature:doSomethingSpecial()
  -- ...
end

When this is tried without any additional C++ code modifications, it result in an error No writable member 'doSomethingSpecial'.

I did some experimentation based on this docs section
https://kunitoki.github.io/LuaBridge3/Manual#27---index-and-new-index-metamethods-fallback
and turns out I can handle calling custom method by using addIndexMetaMethod, but to actually add that custom method I had to use .addStaticFunction("__newindex", ...), which I'm not even sure is correct to use, because it's not documented. As method is added for class binding table, not for instance, addNewIndexMetaMethod has no use here.

I can share my experimental code if it helps, but maybe correct solution to this problem is known already, so I'd like to ask about it first.

@rpatters1
Copy link
Contributor

Check out mixin.lua at the Finale Lua Scripts repo. It does exactly what you want to do. It extends the class in Lua.

Odd that you should write today, but I just discovered that mixins are broken out of the box with LB3. That is because of security around letting scripts access __index and __newindex. (See issue #113.) You can enable access in your C code by calling luabridge::setHideMetatables(false); in your program initialization. However that seems like it is a security hole.

If you find a solution that does not require exposing the metatables to script, let us know. I've been looking at whether some sort of forwarding is possible, but my Lua API chops will take some polishing to get there, I expect.

@kunitoki
Copy link
Owner

Setting a static __newindex method on the type Creature is the only way to be able to extend that exposed class, as you are indeed adding a new method to the class, not an instance of it (which addNewIndexMetaMethod will handle in case no other registered property or method could be found). I'm not sure tho how will you resolve the extended method from an instance of said class, except from accessing a static table of extended methods living in the Creature class from addIndexMetaMethod.

Regarding the metatables access, this is something i will review shortly.

@kunitoki kunitoki added the enhancement New feature or request label Apr 10, 2023
@rpatters1
Copy link
Contributor

rpatters1 commented Apr 10, 2023

I'm not sure tho how will you resolve the extended method from an instance of said class.

The way mixin.lua does it is to replace the bound class's __newindex and __index functions. From the mixin link above, see:

function mixin_private.apply_mixin_foundation(object)

This a cool technique, but it definitely raises security concerns. It really depends how much you trust the Lua code that will be executing it.

@dtroitskiy
Copy link
Author

dtroitskiy commented Apr 10, 2023

I'm not sure tho how will you resolve the extended method from an instance of said class, except from accessing a static table of extended methods living in the Creature class from addIndexMetaMethod.

In my experimental code first I tried to just create static std::unordered_map<LuaRef, LuaRef> container based on example from docs and store LuaRef to added class methods there, then I decided to try this, not sure it's correct thing to do:

.addStaticFunction("__newindex", [](const LuaRef& classTable, const LuaRef& key, const LuaRef& value, lua_State* L)
{
	classTable.getMetatable()[key] = value;
	return LuaRef(L, LuaNil());
})

And then I do

.addIndexMetaMethod([](FlexibleClass& self, const LuaRef& key, lua_State* L)
{
	auto metatable = getGlobal(L, "FlexibleClass").getMetatable();
	if (metatable[key])
	{
		return metatable[key].cast<LuaRef>().value();
	}
})

And then it works this way for calling added method on instance.
I think I don't even need to use getMetatable() here, I could use any suitable container, I guess, just felt more correct from Lua point of view to add new methods there.

About mixin.lua, I will need to look at it more closely, I guess. For now I don't understand whether it may simplify life for me, or I'll just be okay with using __newindex.

@kunitoki
Copy link
Owner

Yes this makes sense, i think it's a good approach for what you want to achieve.

@rpatters1
Copy link
Contributor

The main thing about mixin.lua is that it extends the C++ classes without their knowledge. One of things I'm, not sure about in your experimental code is how to handle arbitrary numbers and types of parameters. Do they flow through on the Lua stack, or do you handle them some other way?

@dtroitskiy
Copy link
Author

@rpatters1 you mean which parameters exactly? In this particular code I don't do any calls from C++ code, I just store reference to custom class method (basically Lua function) passed to .addStaticFunction("__newindex", const LuaRef& classTable, const LuaRef& key, const LuaRef& value, lua_State* L), thus here value is that function. And when this method is called on an instance, I return it from addIndexMetaMethod.

Currently for our project I implemented solution which in the end makes me add 3 lines of code to each class I plan to extend in Lua and so far it looks sufficient to me, which is why I quesiton myself whether I need any more help of third-party library like mixin.lua. I mean, will it bring any more advantages?

@kunitoki just to be clear, is it not technically possible to make it not needing to make custom __newindex method? As I don't know much about LuaBridge internal logic, I'm not sure what design or technical or security limitations you may face. Ideally I imagine this as bound classes may just be extended without any additional C++ modifications, basically like regular Lua tables. If there is some security concern, I can only say that I work with Lua code written by our team, so in this case it's completely trusted. So if this is a matter of security, maybe it would be possible to turn off some security checks with some call to LuaBridge.

@rpatters1
Copy link
Contributor

The advantage of rewriting the metatables is that you don't need to change the C++ code to extend your classes. You can do it in pure Lua. But that's also the disadvantage, in that your classes can be extended without their/your knowledge, so you have to trust the code that's doing it.

I am just referring to mixin as an example of how to do this. You would have to write it yourself. That library is specific to the Finale Lua environment.

@kunitoki
Copy link
Owner

I would make it selectable somehow, you don't always want classes to be extensible. But definately can be abstracted away completely and encapsulated by luabridge.

Regarding security, i think only allowing to change the __gc for a custom userdata is a potential risk, but i'm sure injecting a malicious method (if you allow bytecode, unguarded i/o, loading compiled lua libraried at runtime and anything reported here as unsafe) it could in theory keep the door open for malicious code. Just hiding metatables isn't enough, if you want better security you should look at compiling the lua vm into a wasm binary with only a subset of wasi exposed (so any access to host memory is guarded).

@Erza
Copy link

Erza commented Apr 11, 2023

This is more an issue with the LuaBridge library here, and should be addressed in LuaBridge, without needing any 3rd party stuff, like that mixin library to be involved. It shouldn't even be considered as a viable solution to the issue at hand here.

@kunitoki
Copy link
Owner

Well there are no issues in luabridge regarding this and @dtroitskiy already shown that is possible with the available tools. This is more of a request for making things nicer from an api perspective.

@kunitoki kunitoki linked a pull request Apr 13, 2023 that will close this issue
@kunitoki
Copy link
Owner

kunitoki commented Apr 13, 2023

@dtroitskiy it would be nice if you could run some testing on #116

I will run some test on explicit metamethods overrides (like overriding __gc and the like).
EDIT: Seems like it should be fine, those methods will not overwrite metatables methods.

@dtroitskiy
Copy link
Author

Tested it, great stuff! At first I didn't know what I need to modify in my code, then saw in tests it's just one option luabridge::extensibleClass, and now calling custom methods works without any custom __newindex / __index handlers in C++, that's really nice.
Now completely awesome would be to also allow custom properties to work transparently. Because yeah, calling custom methods works, but when I try this:

function ExtensibleClass:setSomething(smth)
  self.something = smth
end

I still get no writable member 'something'. And of course I realize I could still use __newindex / __index to make this work, but if it's possible to make it work by adding single option, then it should be everything we need thus far.

@kunitoki
Copy link
Owner

kunitoki commented Apr 14, 2023

Now completely awesome would be to also allow custom properties to work transparently.

Yes this is next step, will work on it. For now i just wanted to have methods working.

But this could be a bit more tricky to get right, as we have a userdata (not a table) for self, so i'm not sure i can treat it as a table (i can't assign smth to its metatable or all instances of ExtensibleClass will now inherit smth, which is not what we want). Attaching methods is fine because those are added to the metatable, but property values are attached to the instance. Ideas welcome.

@ThistleSifter
Copy link

Is using additional storage an option, using the userdata as a weak key?

Bound classes can have properties that are readable or readable/writable and also only accept certain types. Is it possible to have the same level of control with custom properties without using __index and __newindex? Maybe with the ability to define get/set functions, sort of like JavaScript classes?

@kunitoki
Copy link
Owner

I'm not a fan of global storage, and i wouldn't see myself using a table in the type metatable to store instance data, or a table in the registry indexed by type or by weak userdata

@dtroitskiy
Copy link
Author

Hm, yeah, I understand the problem. In my current code I wrote before the improvements I basically followed this example https://kunitoki.github.io/LuaBridge3/Manual#27---index-and-new-index-metamethods-fallback (with the difference I also added static __newindex for adding custom methods), and thus I used a container for custom properties std::map<luabridge::LuaRef, luabridge::LuaRef> properties;. In the end I made a class which I inherit from when I need a derived class to be extensible.

With the new solution in LuaBridge I understand that without modifications in C++ code it's not easy to determine what is the best way to store custom properties.

If some global storage is not a good option, what about if having a container for custom properties becomes mandatory for C++ class? Yes, this adds a bit of manual work for C++ side, but also makes the code cleaner, I suppose.
So the idea is the following:

class ExtensibleClass
{
private:
  std::map<luabridge::LuaRef, luabridge::LuaRef> properties;
};

// Bindings:
getGlobalNamespace(luaState)
  .beginClass<ExtensibleClass>("ExtensibleClass")
  .addCustomPropertyContainer(&ExtensibleClass::properties)
  // ...

You can, of course invent any other name instead of addCustomPropertyContainer().

@kunitoki
Copy link
Owner

I was thinking that when i allocate the userdata to hold the class instance, i could in theory use a special Userdata<T>, something like ExtensibleUserdata<T> which could provide the key value store together with the instance. Indeed it's a nice challenge 😄

@kunitoki
Copy link
Owner

kunitoki commented Apr 17, 2023

Btw i'm considering removing addIndexMetaMethod and addNewIndexMetaMethod from the public interface, the same could be achieved with registering __index and __newindex methods in the class registration. This way there's no clash with the extensible classes usage of them.

Not possible with extensible classes

@Mellnik
Copy link

Mellnik commented Apr 17, 2023

Btw i'm considering removing addIndexMetaMethod and addNewIndexMetaMethod from the public interface, the same could be achieved with registering __index and __newindex methods in the class registration. This way there's no clash with the extensible classes usage of them.

Sounds good, there is no need for these functions then.

@kunitoki
Copy link
Owner

Apparently it doesn't seem possible, replacing __index and __newindex directly is not possible when using extensible classes, so i will have to revert it.

@kunitoki
Copy link
Owner

kunitoki commented Apr 17, 2023

I think for now, this gives the best flexibility (in term of storage and customizability of the properties lookup, as you might have a map or an unordered_map, or a custom other container, imagine you want to use a QHash when using Qt and so on...). Methods is different as they are stored in the class metatable anyway, but properties don't.

    luabridge::getGlobalNamespace(L)
        .beginClass<ExtensibleBase>("ExtensibleBase", luabridge::extensibleClass)
            .addConstructor<void(*)()>()
            .addFunction("baseClass", &ExtensibleBase::baseClass)
            .addIndexMetaMethod([](ExtensibleBase& self, const luabridge::LuaRef& key, lua_State* L)
            {
                auto metatable = luabridge::getGlobal(L, "ExtensibleBase").getMetatable();
                if (auto value = metatable[key])
                    return value.cast<luabridge::LuaRef>().value();

                auto it = self.properties.find(key);
                if (it != self.properties.end())
                    return it->second;

                return luabridge::LuaRef(L, luabridge::LuaNil());
            })
            .addNewIndexMetaMethod([](ExtensibleBase& self, const luabridge::LuaRef& key, const luabridge::LuaRef& value, lua_State* L)
            {
                self.properties.emplace(std::make_pair(key, value));
                return luabridge::LuaRef(L, luabridge::LuaNil());
            })
        .endClass();

    runLua(R"(
        function ExtensibleBase:test()
            return 41 + self.xyz + self:baseClass()
        end

        local base = ExtensibleBase()
        base.xyz = 1000
        result = base:test()
    )");

Later on i can see if providing a luabridge::PropertiesClass to be use a base class, then when registering a type derived from it, we can detect it automatically and expose those methods as you suggest.

@Mellnik
Copy link

Mellnik commented Apr 18, 2023

I do have one concern tho.

luabridge::LuaRef is a nice wrapper around the registry. But I think that the base implementation of luabridge should not depend on it. In my project I do now have my own wrapper that is seamlessly integrated with the rest of the system. For example it automatically takes care when a lua_State is gone.

So in this case I push my stored value on the stack and then it gets picked up my LuaRef again which is a bit unnecessary.
Just wanted to raise your awareness that there can be different complex integrations. Maybe you can take that into consideration when designing the API.

@kunitoki
Copy link
Owner

kunitoki commented Apr 18, 2023

Indeed, but you can also implement a simple lua_CFunction for the index + newindex fallback metamethods and manage that with simple lua calls. That is user code, luabridge assumes it is just a callable supported, and because LuaRef can be obtained from values on the stack, that works with it

@dtroitskiy
Copy link
Author

I think for now, this gives the best flexibility ...

Yep, that's what I basically implemented as my first solution. The only inconvenience here is that, as I wanted extensibility to be shared across many classes, I made it a base class, and then you have to somehow provide the name for luabridge::getGlobal(L, "ExtensibleBase").getMetatable(); (i.e. "ExtensibleBase" here), so I made a method setClassName(), which is called from inheritant constructors.

By the way, deriveClass() doesn't support providing more than one base class, does it?
Because if it doesn't, that's what I was thinking to create another issue about. I understand that multiple inheritance is considered a bad practice in general, but as C++ supports it, would be nice to have support for it in deriveClass(), if possible. Because in my case I would inherit from Extensible without putting it somewhere on top of my inheritance tree, as I do currently.

@kunitoki
Copy link
Owner

No, there is no multiple inheritance support for binded c++ classes in luabridge, but definately this is something that could be added with minimal effort.

@dtroitskiy
Copy link
Author

I thought re-opening this would be better than creating new issue, since it's quite relevant.
So, with extensibleClass we're able to add custom methods, and by supplying additional container for storing LuaRefs on C++ side, accompanied by addIndexMetamethod() / addNewIndexMetamethod() we're able to store custom instance properties.

But turned out we're unable to store custom class properties, i.e. static properties.
So if you try to do in Lua (Test is bound class):

Test.t = 1
print(Test.t)

It fails with exception in CFunctions.h, line 260:

LUABRIDGE_ASSERT(lua_isnil(L, -1)); // Stack: mt, nil
lua_pop(L, 1); // Stack: mt <--- HERE

At the same time, if I do it the old way like I did before with addStaticFunction("__index"), ... (in this comment #114 (comment)) then I'm able to make it work again. But this looses all the point of having extensibleClass, obviosuly. So I assume this should be an easy fix in LuaBridge code.

@kunitoki
Copy link
Owner

mmmh it really depends on how you are resolving the call, can you use luabridge::debug::dumpState to check what's going on there ?

@dtroitskiy
Copy link
Author

If I call luabridge::debug::dumpState() inside an exception handler it simply prints stack #1: "property" and nothing more.

@dtroitskiy
Copy link
Author

Or if I add it as a first line in addIndexMetaMethod() handler, then it prints:

stack #1: userdata@000002A9FFA88ED8
stack #2: "property"

@kunitoki
Copy link
Owner

kunitoki commented Jul 27, 2023

Which line in lua produces this ? i suspect this is accessing Test.property ? Also how is your class registered, i need a full working example, the possibilities are too many

@kunitoki
Copy link
Owner

I have this test working. Can we modify this test in order to check all your specific edge cases ?

TEST_F(ClassExtensibleTests, IndexFallbackMetaMethodFreeFunctorOnClass)
{
    auto indexMetaMethod = [&](OverridableX& x, const luabridge::LuaRef& key, lua_State* L) -> luabridge::LuaRef
    {
        auto it = x.data.find(key);
        if (it != x.data.end())
            return it->second;

        return luabridge::LuaRef(L);
    };

    auto newIndexMetaMethod = [&](OverridableX& x, const luabridge::LuaRef& key, const luabridge::LuaRef& value, lua_State* L) -> luabridge::LuaRef
    {
        x.data.emplace(std::make_pair(key, value));
        return luabridge::LuaRef(L);
    };

    luabridge::getGlobalNamespace(L)
        .beginClass<OverridableX>("X", luabridge::extensibleClass)
            .addIndexMetaMethod(indexMetaMethod)
            .addNewIndexMetaMethod(newIndexMetaMethod)
        .endClass();

    OverridableX x, y;
    luabridge::setGlobal(L, &x, "x");
    luabridge::setGlobal(L, &y, "y");

    runLua(R"(
        X.xyz = 1

        function X:setProperty(v)
            self.xyz = v
        end

        function X:getProperty()
            return self.xyz
        end

        y.xyz = 100
        x:setProperty(2)

        result = x.xyz + y.xyz
    )");
    ASSERT_EQ(102, result<int>());
}

@kunitoki
Copy link
Owner

For example, overwriting a lua extended method should be possible or not ?

    runLua(R"(
        function X:property(v)
            self.property = v
        end

        x:property(2)

        result = x.property
    )");
    ASSERT_EQ(2, result<int>());

@dtroitskiy
Copy link
Author

dtroitskiy commented Jul 27, 2023

Which line in lua produces this ? i suspect this is accessing Test.property ? Also how is your class registered, i need a full working example, the possibilities are too many

No, it's without Test.property line, so it's like this:

function Test:getProperty()
  return self.property
end

function Test:setProperty(value)
  self.property = value
end

local test = Test()
test:setProperty(2)
print(test:getProperty())

Calling test:getProperty() results in accessing self.property, and then addIndexMetaMethod() is called, where I try to access metatable like you suggested. Full class registration is this:

getGlobalNamespace(L)
  .beginClass<Test>("Test", extensibleClass)
  .addConstructor<void()>()
  .addIndexMetaMethod(
    [](Test& self, const LuaRef& key, lua_State* L)
    {
      // Testing metatable access.
      auto metatable = LuaRef::fromStack(L, -2).getMetatable();
      if (metatable.isTable() && !metatable[key].isNil())
      {
        return metatable[key].cast<LuaRef>().value();
      }
      
      auto it = self.properties.find(key);
      if (it != self.properties.end())
      {
        return it->second;
      }
      return LuaRef(L, LuaNil());
    }
  )
  .addNewIndexMetaMethod(
    [](Test& self, const LuaRef& key, const LuaRef& value, lua_State* L)
    {
      self.properties[key] = value;
      return LuaRef(L, LuaNil());
    }
  )
  .endClass();

@dtroitskiy
Copy link
Author

dtroitskiy commented Jul 27, 2023

I pulled latest changes in your branch, and at first I was a bit confused, because I noticed addIndexMetaMethod() / addNewIndexMetaMethod() were called again when test:getProperty() and test:setProperty() were called, while previously they weren't called, and I thought it's not correct again, because they should be resolved from the class, but then I realized you probably implemented my second proposal about calling them first and then trying to resolve from class if they return LuaNil().
And actually looks like my test is fully working. So class property is kinda inherited by instances this way, but at the same time you can override it. So this test:

Test.property = 1

function Test:getProperty()
  return self.property
end

function Test:setProperty(value)
  self.property = value
end

print('Class property:', Test.property)
local test = Test()
print('Instance property:', test:getProperty())
test:setProperty(2)
print('Class property again', Test.property)
print('Instance property again:', test:getProperty())

prints 1, 1, 1, 2 for me, as it should. Thus class property remains unchanged, while instance property gets changed (or, more precisely, added to the internal container) after calling setProperty().
And looks like that's exactly what we need, so great job once again!

About

For example, overwriting a lua extended method should be possible or not ?

You mean overriding methods assigned from C++ or something else? That's what allowOverridingMethods did, right? Yeah, it should work this way. Or if you meant overriding previosuly assigned methods from Lua, then yes too, why not.

Honestly can't think of other edge cases right now, I think I can only report again if we stumble upon something else. For now things look good.

@dtroitskiy
Copy link
Author

About metatable access, seems like it becomes irrelevant if we're able to achieve what we wanted without additional customizations, but still would be nice to know how it works, did I do something wrong in my test or not.
And I see you already added LuaRef::getClassName() this also may become useful in some circumstances, thanks!

@dtroitskiy
Copy link
Author

dtroitskiy commented Aug 1, 2023

I overlooked one small issue, my bad, I should have tested this right away. But I suppose it's going to be a quick fix.

So, in my code I use base Extensible class for all other extensible classes. By the way, that's why I wanted to have multiple inheritance, to inherit from base classes independently, instead of creating forced chain of inheritance, because I have Scheduler class that has to inherit from Extensible, for example, and there are others. Anyway, that Extensible class only serves now the purpose of providing a container for properties. And, again by the way, don't you still think this functionality can be generalized somehow? Because it's basically about creating std::unordered_map<LuaRef, LuaRef> somewhere and then managing properties through it. But if it's for some reason inconvenient to create such a container in LuaBridge logic, then, I already suggested it long time ago, I think you could just add some binding function like addCustomPropertyContainer(&Test::properties) and then still have get / set logic in LuaBridge. Unless you think it may cause other issues potentially.
Although yeah, it's also not a big deal to have these addIndexMetaMethod() / addNewIndexMetamethod() bindings, but dealing with it in a more automated way would be even better.

Now the actual issue is that handler assigned by addIndexMetaMethod() is not called in the base class, while handler for addNewIndexMetamethod() IS called.
The test C++ code for this is:

class Extensible
{
public:
  LuaRef getProperty(const LuaRef& key, lua_State* L)
  {
    auto it = properties.find(key);
    if (it != properties.end())
    {
      return it->second;
    }
    return LuaRef(L, LuaNil());
  }

  LuaRef setProperty(const LuaRef& key, const LuaRef& value, lua_State* L)
  {
    properties[key] = value;
    return LuaRef(L, LuaNil());
  }
  
private:
  unordered_map<LuaRef, LuaRef> properties;
};

class Test : public Extensible
{
public:
  inline Test() {};
};

// ...

  getGlobalNamespace(L)
    .beginClass<Extensible>("Extensible", extensibleClass)
    .addIndexMetaMethod(&Extensible::getProperty) // <-- Doesn't get called.
    .addNewIndexMetaMethod(&Extensible::setProperty) // <-- Called fine.
    .endClass()
    .deriveClass<Test, Extensible>("Test", extensibleClass)
    .addConstructor<void()>()
    .endClass();

And the Lua code is the same as before.

@kunitoki
Copy link
Owner

kunitoki commented Aug 1, 2023

I just tried it now and it works for me, this is the scripts i executed (i just renamed Test with DerivedExtensible):

        function DerivedExtensible:getProperty()
          return self.property
        end

        function DerivedExtensible:setProperty(value)
          self.property = value
        end

        local test = DerivedExtensible()
        test:setProperty(2)
        result = test:getProperty()

and

        local test = DerivedExtensible()
        test.property = 3
        result = test.property

in both cases, the parent Extensible::getProperty and Extensible::setProperty are both called

@kunitoki
Copy link
Owner

kunitoki commented Aug 1, 2023

And, again by the way, don't you still think this functionality can be generalized somehow? Because it's basically about creating std::unordered_map<LuaRef, LuaRef> somewhere and then managing properties through it. But if it's for some reason inconvenient to create such a container in LuaBridge logic, then, I already suggested it long time ago, I think you could just add some binding function like addCustomPropertyContainer(&Test::properties) and then still have get / set logic in LuaBridge. Unless you think it may cause other issues potentially.

Yes, i will think on a way to make this transparent, maybe we don't even require to enter into c++ and we could pin the properties in a hidden lua table attached to the instance.

@kunitoki
Copy link
Owner

kunitoki commented Aug 1, 2023

This is the test:

namespace {
class NonExtensible
{
public:
    luabridge::LuaRef getProperty(const luabridge::LuaRef& key, lua_State* L)
    {
        auto it = properties.find(key);
        if (it != properties.end())
            return it->second;

        return luabridge::LuaRef(L);
    }

    luabridge::LuaRef setProperty(const luabridge::LuaRef& key, const luabridge::LuaRef& value, lua_State* L)
    {
        properties.emplace(key, value);
        return luabridge::LuaRef(L);
    }

private:
    std::unordered_map<luabridge::LuaRef, luabridge::LuaRef> properties;
};

class DerivedExtensible : public NonExtensible
{
public:
    inline DerivedExtensible() {};
};
} // namespace

TEST_F(ClassExtensibleTests, IndexAndNewMetaMethodCalledInBaseClass)
{
    luabridge::getGlobalNamespace(L)
        .beginClass<NonExtensible>("NonExtensible")
            .addIndexMetaMethod(&NonExtensible::getProperty)
            .addNewIndexMetaMethod(&NonExtensible::setProperty)
        .endClass()
        .deriveClass<DerivedExtensible, NonExtensible>("DerivedExtensible", luabridge::extensibleClass)
            .addConstructor<void()>()
        .endClass();

    runLua(R"(
        function DerivedExtensible:getProperty()
          return self.property
        end

        function DerivedExtensible:setProperty(value)
          self.property = value
        end

        local test = DerivedExtensible()
        test:setProperty(2)
        result = test:getProperty()
    )");

    EXPECT_EQ(2, result<int>());

    runLua(R"(
        local test = DerivedExtensible()
        test.property = 3
        result = test.property
    )");

    EXPECT_EQ(3, result<int>());
}

@dtroitskiy
Copy link
Author

I just checked, again it happens when you have Test.property = 1 (in your case DerivedExtensible.property = 1) in the beginning, then getProperty() doesn't get called, otherwise it works good.

@dtroitskiy
Copy link
Author

Yes, i will think on a way to make this transparent, maybe we don't even require to enter into c++ and we could pin the properties in a hidden lua table attached to the instance.

Yep, I was also thinking about it today, that if you can create new table for each instance of a class and somehow associate that table with that instance, then C++ container won't be necessary.

@kunitoki
Copy link
Owner

kunitoki commented Aug 2, 2023

But they shouldn't be called, the index / new index metamethods enter into action when accessing an instance of Test, while Test.property = 1 is accessing the static Test which in your case has no __index nor __newindex. This is a common error when working with lua, to explain this in pseudo code:

Test = {
  __newindex: function() print(1); end -- This is called on global `Test` table
} 

setmetatable(Test, {
  __newindex: function() print(2); end -- This is called on an *instance* of Test 
})

Test.property = 1 -- Prints 1

t = Test.new() -- Assuming we have a constructor
t.property = 1 -- Prints 2

@dtroitskiy
Copy link
Author

No, no, I understand all that, I don't expect addIndexMetamethod() handler to be called when accessing the class. Just adding this line, Test.property = 1 also breaks it for instance, so handler is also not called when accessing self.property inside Test:getProperty() or simply test.property on an instance. Moreover, this all happens now only when inheritance is involved, without it everything works as I expect.

@dtroitskiy
Copy link
Author

So, to be 100% clear, in your test if you add this line:

    runLua(R"(
        DerivedExtensible.property = 1
    
        function DerivedExtensible:getProperty()
          return self.property
        end

        function DerivedExtensible:setProperty(value)
          self.property = value
        end

        local test = DerivedExtensible()
        test:setProperty(2)
        result = test:getProperty()
    )");

I didn't run this myself, but as I'm having totally the same code, I'm pretty sure the test will just fail, while it shouldn't.

@kunitoki
Copy link
Owner

kunitoki commented Aug 2, 2023

I understood, but it's a problem of how should i interpret an assignment to the "static" table (like DerivedExtensible):

DerivedExtensible.property = 1
-- and `function DerivedExtensible:getProperty() end` which will be rewritten as:
DerivedExtensible.getProperty = function(self) end

They are exactly the same, and both will end up writing into the properties property and getProperty of the metatable of the extensible class DerivedExtensible, and the same applies for reading. This means that if the class with the property storage (with index and newindex fallback metamethod) is higher in the chain of inheritance, it will not be reached.

I'm considering completely removing the fallback metamethods and handle it completely inside the extensibleClass. Let's see what i can come up with.

@kunitoki
Copy link
Owner

kunitoki commented Aug 2, 2023

Have hacked a new version which should work on these edge cases, but i'm pretty sure there are other incompatibilities, especially with properties, will do more tests later on

#139

@dtroitskiy
Copy link
Author

dtroitskiy commented Aug 3, 2023

Ok, #139 works for my needs, I guess it should be fine for now, but if you would manage to improve it further, it would be cool.
Thanks as always!

About

This means that if the class with the property storage (with index and newindex fallback metamethod) is higher in the chain of inheritance, it will not be reached.

isn't it strange in this case that only addIndexMetamethod() handler is not called, but addNewIndexMetamethod() handler is called? Because I supposed, if there's a problem, then both handlers shouldn't be called.

@kunitoki
Copy link
Owner

kunitoki commented Aug 3, 2023

It's a bit complicated because of the way lb3 manages registered classes and their metatables, but that was a problem with the static vs instance invocation.

@dtroitskiy
Copy link
Author

@kunitoki, a question.
Is it possible somehow to correctly create a derived class in Lua while also maintaining its extensibility?
Let's say I have Base bound from C++, and the I want to define Derived right in Lua.
My naive approach was like

Derived = {}
setmetatable(Derived, getmetatable(Base))

but then if I do

function Derived:someMethod()
 --
end

this method also gets defined on Base, because I assume internally there methods are stored somewhere on class metatable, and that metatable is accessed by class name. So I guess I need to somehow change this name, and then it will probably work fine.

@kunitoki
Copy link
Owner

kunitoki commented Sep 30, 2023

I don't think this is actually a possibility that have been thought so far, it might work but there we need to actually introduce some kind of internal registration for the C++ side. Applying a c++ bound metatable of a class exposed as a metatable of a non c++ bound lua "class" might introduce all sort of issues that i cannot predict right now. I will think about it if we have an easy way to allow it, but still making luabridge internals know about the lua class "deriving" from the c++ class...

@dtroitskiy
Copy link
Author

I see, well, I'd appreciate if you look into it, because I made a workaround for my needs that kinda wraps C++ class into Lua class (just a table), and, although it's fine for the most part, it still doesn't look very good.
And in general I feel like a possibility to do that may be useful to other LuaBridge users.

@dtroitskiy
Copy link
Author

Hey @kunitoki! For a long time I haven't posted any new issues, but now I have to get back again to this long-suffering issue =)

So, one thing I noticed recently with extensibleClass is that if you have a class inheriting from another class, and you define a method with the same name in both classes, they both will be calling a method that was defined last. Something like:

 function Base:init()
   print('Base:init()')
 end

function Derived:init()
   print('Derived:init()')
 end 
 
 local base = Base()
 base:init() -- will call Derived:init()

And we assume here that it was .deriveClass<Derived, Base>("Derived") in C++.

To fix it, at first I didn't want to bother you, but wanted to use custom property container once again, since I use it anyway for custom instance properties, but this time I wanted to use it for static properties (like these methods). So I got back to using __index / __newindex handlers. And I create custom property container right in the scope of the function where I do bindings and it works mostly fine. Code looks like this:

static auto staticProperties = newTable(luaState);

getGlobalNamespace(luaState)
 .beginClass<Color>("Color", visibleMetatables)
 .addStaticFunction(
   "__index",
   [](const LuaRef& classTable, const LuaRef& key) -> LuaRef
   {
     const auto& value = classTable.getMetatable()[key];
     if (!value.isNil())
     {
       return value;
     }
     return staticProperties[key];
   }
 )
 .addStaticFunction(
   "__newindex",
   [](const LuaRef& classTable, const LuaRef& key, const LuaRef& value)
   {
     staticProperties[key] = value;
     return LuaRef(LuaState::get());
   }
 )

But the problem I encountered with this approach is that now addStaticProperty doesn't work for me, because I see in LuaBridge static properties are bound with some lua_pushcclosure_x() etc. and I have no idea how to access that data from my custom __index handler.

So I think it would be nice, and I might have suggested this before, to add addStaticIndexMetamethod() / addStaticNewIndexMetamethod() to work the same way as non-static versions work, i.e. try default behavior first, and then fallback to these handlers, if they're bound.

About the initial problem. Currently I removed extensibleClass option from my bindings, since I use these custom static property containers. But if you can also fix this problem when methods with the same name overwrite each other in the inheritance chain, it will be nice either way. Though I must say, I keep thinking that approach of having custom property containers added from C++ seems nice to me. It kinda solves extensibility problem just by using one container for static properties and another one for instance properties (and when calling methods on an instance, you also have to lookup in static property container). Because otherwise you have to mess with metatables in Lua, which seems like less transparent approach.

To conclude, personally for me having addStaticIndexMetamethod() / addStaticNewIndexMetamethod() would be perfectly enough for having extensibility. But you can also consider again that proposal I made long time ago: to make some methods like addStaticPropetyContainer() / addInstancePropertyContainer() which will refer to variables / class fields used to store custom properties, like in my example code I used newTable, but it also can be std::map<LuaRef, LuaRef>.

If you need me to provide a complete example with these issues illustrated, I can, for now was a bit busy to do that.

@kunitoki
Copy link
Owner

Hey there ! I see, yes for static properties to work reliably we need to have addStaticIndexMetamethod / addStaticNewIndexMetamethod. Will see what i can do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
6 participants