ThePhD / sol2

Sol3 (sol2 v3.0) - a C++ <-> Lua API wrapper with advanced features and top notch performance - is here, and it's great! Documentation:
http://sol2.rtfd.io/
MIT License
4.06k stars 492 forks source link

How can I create a usertype but override certain returned properties as raw getter functions? #1558

Closed GasimGasimzada closed 6 months ago

GasimGasimzada commented 7 months ago

I have a struct that has no methods inside it:

struct UIViewStyle {
  f32 grow = 0.0f;

  f32 shrink = 1.0f;

  YGFlexDirection direction{YGFlexDirectionRow};

  YGJustify justifyContent{YGJustifyFlexStart};

  YGAlign alignItems{YGAlignStretch};

  YGAlign alignContent{YGAlignFlexStart};

  glm::vec4 backgroundColor{0.0f};
};

I want to expose this via Sol but modify what's returned for some of the properties without adding getter functions inside the struct. I like to keep the structs as structs without any methods inside them.

Is there a way that I can create a free function that accepts the property as a getter and returns a string value (YG* values are all enums)? Something like this:

auto uiViewStyle =
      state.new_usertype<UIViewStyle>("UIViewStyle", sol::no_constructor);
uiViewStyle["direction"] = sol::property([](UIViewStyle style) {
  return convertDirectionToString(style.direction);
});
-- `YGFlexDirectionRow` automatically returned as "row" instead of integer
style.direction == "row"

Or is it possible to create strings from enums; so, whenever I pass enum to Lua or retrieve an enum from Lua, I always work with strings instead of C++ enums, which are integers in this case:

auto uiViewStyle =
      state.new_usertype<UIViewStyle>("UIViewStyle", sol::no_constructor);
uiViewStyle["direction"] = &uiViewStyle::direction;
-- `YGFlexDirectionRow` automatically returned as "row" instead of integer
style.direction == "row"
Rochet2 commented 6 months ago

What is wrong with the first example you give? I think that (or something very very very close to it) should work.

GasimGasimzada commented 6 months ago

That does not work because sol::property does not accept an argument. What I need to do is to create a class that has a getDirection function, then use that as the property:

struct UIViewStyle {
  String getDirection() {
    converDirectionToString(this->direction);
  }

  YGDirection direction = YGFlexDirectionRow;
};

But this is an antipattern because my UIViewStyle struct is a plain old struct with no data in it and I essentially need to either store the entire serialization as a member function of the class or create a Lua specific proxy class that maps to this, which becomes very complex when this object is part of another structure, which I will need to create Lua proxy class as well.

Rochet2 commented 6 months ago

What I need to do is to create a class that has a getDirection function, then use that as the property

In general for sol, you can do all kinds of magic in the function signature that sol will recognize and act upon. For example, you can add an argument to have access to the lua state like this: https://sol2.readthedocs.io/en/latest/api/this_state.html

For your case you can bind a function that takes in a UIViewStyle& or UIViewStyle* or something similar (both work, because sol magic).

Here is a working example (godbolt link):

```cpp #include #define SOL_ALL_SAFETIES_ON 1 #include #include struct UIViewStyle { float grow = 0.0f; float shrink = 1.0f; int direction = 1; }; const char* YGFlexDirectionToString(int direction) { switch (direction) { case 1: return "column"; case 2: return "column-reverse"; case 3: return "row"; case 4: return "row-reverse"; default: return "unknown"; } } int YGFlexStringToDirection(const char* direction) { std::string directionStr(direction); if (directionStr == "column") { return 1; } else if (directionStr == "column-reverse") { return 2; } else if (directionStr == "row") { return 3; } else if (directionStr == "row-reverse") { return 4; } else { throw std::invalid_argument("Unknown direction"); } } int main() { sol::state lua; lua.open_libraries(); auto uiviewstyle_mt = lua.new_usertype("UIViewStyle", "new", sol::no_constructor ); uiviewstyle_mt["direction"] = sol::property( // getter [](UIViewStyle& style) { return YGFlexDirectionToString(style.direction); }, // setter [](UIViewStyle& style, const char* direction) { style.direction = YGFlexStringToDirection(direction); } ); lua["style"] = UIViewStyle(); lua.script(R"( print(style.direction) -- column -- style.direction = 4 -- error: expected string, received number -- style.direction = "foobar" -- error: Unknown direction style.direction = "row-reverse" -- properly sets the struct value to an integer print(style.direction) -- row-reverse )"); return 1; } ```
Rochet2 commented 6 months ago

In general all functions tied to lua userdata are just free functions that pass the data forward. It is possible to tap into that in sol as well as seen above, which can allow you to make quite free and interesting choices in the way you bind things. However, sol also has abstractions that allow you to just bind the member functions and variables directly for ease of use.

GasimGasimzada commented 6 months ago

@Rochet2 Thank you for the help! I had no idea if this was possible and there are so many places where I needed to do this and ended up creating lots of adapter/proxy classes to handle that. Now, I have removed those dependencies and made them significantly simpler using this method.