-
-
Notifications
You must be signed in to change notification settings - Fork 3
Storing arbitrary types as a global? #1
Would it be possible to store arbitrary first-party types as globals via SetGlobal<T>, perhaps assisted with attributes?
My use case for Laylua is as an intermediate scripting language for a Discord bot, and I would like people writing their scripts to have access to a context "state" at the time the script is called. To my understanding, Lua supports an extremely limited but serviceable way to implement classes via tables, and I am currently accomplishing it via a custom ILuaLibrary and this monstrosity (which isn't even completed yet):
public void Open(Lua lua, bool leaveOnStack) { IGuildChannel? channel = _context.Bot.GetChannel(_context.GuildId, _context.ChannelId); var guild = _context.Bot.GetGuild(_context.GuildId)!; lua.Execute(@$" ctx = {{ author = {{ id = ""{_context.Author.Id}"" name = ""{_context.Author.Name}"" nick = {(string.IsNullOrWhiteSpace(_context.Author.Nick) ? $"\"{_context.Author.Nick}\"" : "nil")} tag = ""{_context.Author.Tag}"" avatar = ""{_context.Author.GetGuildAvatarUrl()}"" created = {_context.Author.CreatedAt().ToUnixTimeSeconds()} }} channel = {{ id = ""{_context.ChannelId}"" type = {(channel is not null ? $"\"{channel.Type}\"" : "nil")} name = {(channel is not null ? $"\"{channel.Name}\"" : "nil")} created = {_context.ChannelId.CreatedAt.ToUnixTimeSeconds()} }} guild = {{ id = ""{guild.Id}"" name = ""{guild.Name}"" created = ""{guild.CreatedAt().ToUnixTimeSeconds()}"" }} }}"); }
This allows scripts to access ctx and all sub-properties such as ctx.author.id as a pseudo-global. It would be nice to somehow have first-class support for this in the library, but if this would incur a performance hit you are not OK with due to reflection or other nonsense, that's fine. As you can see I have something that "works", even if it is a bit ugly. I could imagine this being implemented as a sort of "Lua source generator" of sorts which takes a T, and converts it into an appropriate Lua table as long as all the properties can either be expressed as primitives, or another table?
All reactions
UD Descriptors
Yes, this functionality is already supported with user data descriptors.
However, I haven't yet implemented the attribute-based approach for it. It requires significant effort to ensure efficient and customizable implementation, which means that, for now, you'll need to manually implement the necessary boilerplate code yourself.
Here's a simple example that demonstrates getting and setting a .NET property with Lua through a descriptor:
using (var lua = new Lua()) { lua.Marshaler.UserDataDescriptorProvider.SetDescriptor(typeof(MyClass), new MyClassUserDataDecriptor()); lua.SetGlobal("myclass", new MyClass()); lua.Execute("myclass.text = 'Hello, World!'");...
Replies: 1 comment 1 reply
UD Descriptors
Yes, this functionality is already supported with user data descriptors.
However, I haven't yet implemented the attribute-based approach for it. It requires significant effort to ensure efficient and customizable implementation, which means that, for now, you'll need to manually implement the necessary boilerplate code yourself.
Here's a simple example that demonstrates getting and setting a .NET property with Lua through a descriptor:
using (var lua = new Lua()) { lua.Marshaler.UserDataDescriptorProvider.SetDescriptor(typeof(MyClass), new MyClassUserDataDecriptor()); lua.SetGlobal("myclass", new MyClass()); lua.Execute("myclass.text = 'Hello, World!'"); var result = lua.Evaluate<string>("return myclass.text"); Console.WriteLine(result); // Hello, World! }
Remaining Code
Note how both
IndexandNewIndexin the descriptor are checking whether the key is a string. This is necessary because in Lua usingmyclass.keyis just a shorthand notation formyclass['key']. In other words, Lua allows indexing with various types of values.
public class MyClass { public string? Text { get; set; } }
public class MyClassUserDataDecriptor : CallbackBasedUserDataDescriptor { public override string MetatableName => nameof(MyClass); // Specifies which callbacks the descriptor supports public override CallbackUserDataDescriptorFlags Flags { get { // Supports both getting and setting properties; return CallbackUserDataDescriptorFlags.Index | CallbackUserDataDescriptorFlags.NewIndex; } } public override int Index(Lua lua, LuaStackValue userData, LuaStackValue key) { var instance = userData.GetValue<MyClass>()!; // Check for `myclass.key`: if (key.Type == LuaType.String) { // This could be done with Reflection, for example: switch (key.GetValue<string>()) { case "text": { lua.Stack.Push(instance.Text); return 1; // The amount of values returned, i.e. the amount of values we pushed onto the stack. } } } return 0; } public override int NewIndex(Lua lua, LuaStackValue userData, LuaStackValue key, LuaStackValue value) { var instance = userData.GetValue<MyClass>()!; // Check for `myclass.key = value`: if (key.Type == LuaType.String) { // This could be done with Reflection, for example: switch (key.GetValue<string>()) { case "text": { if (!value.TryGetValue<string>(out var stringValue)) lua.RaiseArgumentTypeError(value.Index, "string"); instance.Text = stringValue; return 1; // The amount of values returned, i.e. the amount of values we pushed onto the stack. } } } return 0; } }
Note that creating tables, as you did, to represent the data is a perfectly valid approach as well.
Both userdata and tables have metatable support.
See Lua manual for more information on metatables and userdata.
Creating the Tables
Instead of using Execute() with interpolated strings to create tables, I suggest using LuaTable which offers the following advantages:
- Reduced chances of mistakes.
- Automatic value marshaling, including support for
long long(Int64orlongin C#) in Lua. This enables you to store Discord IDs as integers instead of strings. - Improved code readability and reusability since
LuaTableessentially functions as a dictionary.
Here's an example of how this could look like:
using (var ctxTable = lua.CreateTable()) { using (var authorTable = lua.CreateTable()) { authorTable.SetValue("id", id); authorTable.SetValue("name", name); ctxTable.SetValue("author", authorTable); } using (var channelTable = lua.CreateTable()) { channelTable.SetValue("id", id); channelTable.SetValue("name", name); ctxTable.SetValue("channel", channelTable); } lua.SetGlobal("ctx", ctxTable); }
Note that when you dispose of the
LuaTableinstances, it only releases the reference to the table within Lua. The object itself becomes eligible for garbage collection when neither you nor Lua have any remaining references to it.
I hope this proves helpful, and thank you for giving the library a try!
All reactions
-
👍 1
For the time being I think I will stick with the LuaTable-based approach, as it while it's not the overengineered automagic marvel something like the first approach might be, it certainly requires a lot less messy boilerplate - I'm not even sure how messy it would get with the whole "nested tables" thing I'm actually using. Thanks for letting me know about the plans for (and current support for) this behavior, I'll be keeping an eye out for if it does come to light - and I'll be more than happy to test it out if or when it does come.