Skip to content

Navigation Menu

Sign in
Sign up

Storing arbitrary types as a global? #1

Pinned Answered by Quahu
QuantumToasted asked this question in Q&A
Discussion options

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?

You must be logged in to vote

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

Comment options

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 Index and NewIndex in the descriptor are checking whether the key is a string. This is necessary because in Lua using myclass.key is just a shorthand notation for myclass['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 (Int64 or long in C#) in Lua. This enables you to store Discord IDs as integers instead of strings.
  • Improved code readability and reusability since LuaTable essentially 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 LuaTable instances, 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!

You must be logged in to vote
1 reply
Comment options

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.

Answer selected by QuantumToasted
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
None yet

AltStyle によって変換されたページ (->オリジナル) /