C
C#•5w ago
croizat

[NLua] How to provide the lua state with a dynamically accessible class

I have a Service class that contains different services that relate to the game, like Player, WorldState, etc. I want to provide access this class to the lua state in a way where it is newly accessed each time it's called. So not just doing
using var lua = new Lua();
foreach (var p in typeof(Service).GetProperties())
lua[p.Name] = p.GetValue(typeof(Service));
using var lua = new Lua();
foreach (var p in typeof(Service).GetProperties())
lua[p.Name] = p.GetValue(typeof(Service));
because that will lead to the values being cached on load, and not accessed whenever they're called in a lua script. The goal is to ultimately be able to just call Service.Player.Position or Service.WorldState:GetMapName() (or formatted reasonably similarly) and fetch the value on call. There are hundreds of functions and properties within the service class so I'm not interested in making wrappers for all of them either and registering them individually. This seems very straightforward to accomplish and that I'm missing someting obvious, but the lack of nlua documentation is getting to me.
16 Replies
Kiel
Kiel•5w ago
so your goal is to achieve something akin to lazy property access? so instead of assigning foo at the moment of intialization, you want to defer fetching that value until the lua code tries to access foo?
croizat
croizatOP•5w ago
Yes, per-access too to be clear
Kiel
Kiel•5w ago
i'm obviously making a lot of assumptions here based on the fact that this is (I assume) for lua scripting in a game/engine, so feel free to correct me: assuming, like many games do, your lua code is executed once per frame/event/etc, is there a particular usecase for you wanting or expecting these properties to return different values each time they are accessed? in the few games I've written lua scripts for generally your events or callbacks are a "snapshot" of the game state at that particular moment so expecting multiple different return values for a property just doesn't make sense to me? technically speaking you could defer this by using Func<T> instead of T directly. Not sure how that would work within NLua...I use Laylua for my Lua scripting needs (looks very similar to your example) but I've never used it in a game 😅
croizat
croizatOP•5w ago
for lua scripting in a game/engine
That would be correct.
is there a particular usecase for you wanting or expecting these properties to return different values each time they are accessed
well yeah, the game is changing every frame after all. Like if you have a lua script where you want to execute something when Servce.Player.ZoneId == 1 you'd just loop checking the value. It wouldn't be helpful if the value is cached on script start or on first access. I do have it setup to be able to trigger scripts on conditions, where you'd really only care about a "snapshot" of the game as you said but it also has the ability to just run scripts alongside the game for an indefinite period of time
Kiel
Kiel•5w ago
then yeah, I would register it as a function than a value. I'm not sure how it works in NLua, but...if these are auto-properties I think you can even use the GetMethod directly...
// service is instance of Service, or use null if static?
lua.RegisterFunction($"get{p.Name}", service, p.GetMethod);
// service is instance of Service, or use null if static?
lua.RegisterFunction($"get{p.Name}", service, p.GetMethod);
I've never used property GetMethods before but I know of their existence. Feel free to mess around with it. if you absolutely insist on making it a property instead of a function in lua there might still be a way to get a new value each time via Func<T>.
croizat
croizatOP•5w ago
Yeah that's something I've tried before, but it really just ended up with null values, probably because I'm not fantastic with reflection. This was one iteration for reference
lua.NewTable("Service");

var svcType = typeof(Svc);
var properties = svcType.GetProperties(BindingFlags.Public | BindingFlags.Static);

foreach (var property in properties)
{
if (property.GetValue(null) is not { } service) continue;

lua.NewTable($"Service.{property.Name}");
var serviceTable = lua.GetTable($"Service.{property.Name}");

var serviceType = service.GetType();
var members = serviceType.GetMembers(BindingFlags.Public | BindingFlags.Instance);

foreach (var member in members)
{
if (member is PropertyInfo prop)
{
serviceTable[prop.Name] = new Func<object>(() =>
{
try
{
return prop.GetValue(service);
}
catch (Exception ex)
{
Svc.Log.Error(ex, $"Error getting {prop.Name}");
}
});
}
else if (member is MethodInfo method)
{
serviceTable[method.Name] = new Func<object[], object>(args =>
{
try
{
return method.Invoke(service, args);
}
catch (Exception ex)
{
Svc.Log.Error(ex, $"Error calling {method.Name}");
}
});
}
}
}
lua.NewTable("Service");

var svcType = typeof(Svc);
var properties = svcType.GetProperties(BindingFlags.Public | BindingFlags.Static);

foreach (var property in properties)
{
if (property.GetValue(null) is not { } service) continue;

lua.NewTable($"Service.{property.Name}");
var serviceTable = lua.GetTable($"Service.{property.Name}");

var serviceType = service.GetType();
var members = serviceType.GetMembers(BindingFlags.Public | BindingFlags.Instance);

foreach (var member in members)
{
if (member is PropertyInfo prop)
{
serviceTable[prop.Name] = new Func<object>(() =>
{
try
{
return prop.GetValue(service);
}
catch (Exception ex)
{
Svc.Log.Error(ex, $"Error getting {prop.Name}");
}
});
}
else if (member is MethodInfo method)
{
serviceTable[method.Name] = new Func<object[], object>(args =>
{
try
{
return method.Invoke(service, args);
}
catch (Exception ex)
{
Svc.Log.Error(ex, $"Error calling {method.Name}");
}
});
}
}
}
Kiel
Kiel•5w ago
I think NLua lets you effectively inject C# objects into your code already...it feels like you might be re-inventing the wheel already. Are they copied by value and don't change every frame, so that's why you're trying to do it via reflection? Laylua has what it calls Value Marshalling which lets you do just that. I asked the dev if it's copied by value or if it's actually pointing directly to the reference (i.e. volatile/can change on access like you want)...haven't heard back yet
croizat
croizatOP•5w ago
I think NLua lets you effectively inject C# objects into your code already...it feels like you might be re-inventing the wheel already.
this very well might be the case and I've wasted hours on this but finding out info about nlua is like pulling teeth
Are they copied by value and don't change every frame, so that's why you're trying to do it via reflection?
as in they're all structs or primitives? Yeah mostly. There are quite a few pointers but I'm mostly not worried about those
Kiel
Kiel•5w ago
What I guess I meant was: if you can figure out how to access c# types from within nlua, see if changing the value on the outside changes the value on the inside. I have a feeling it won't, but it doesn't hurt to try
Kiel
Kiel•5w ago
Sorry for the botched screen shot, but this is on NLua's readme.md
No description
croizat
croizatOP•5w ago
cannot for the life of me figure out doing it like that import statement. I can however do
lua.DoString(@$"luanet.load_assembly('{typeof(T).Assembly.GetName().Name}')");
lua.DoString(@$"{typeof(T).Name} = luanet.import_type('{typeof(T).FullName}')()");
lua.DoString(@$"luanet.load_assembly('{typeof(T).Assembly.GetName().Name}')");
lua.DoString(@$"{typeof(T).Name} = luanet.import_type('{typeof(T).FullName}')()");
Can't test if it actually accesses it on call though since it seems to crash when called in a loop... Works as a one liner at least
Kiel
Kiel•5w ago
if you can find a way to have a safe accessible class that wraps whatever your service is I don't see why you wouldn't be able to just do...
var service = Service.Instance; // or just new Service()...get it however you want
state["Service"] = service;
var service = Service.Instance; // or just new Service()...get it however you want
state["Service"] = service;
or
// assuming Service is in namespace Foo.Bar
state.DoString("import ('Foo.Bar')");
// you should be able to access it in Service.SomeProperty
// assuming Service is in namespace Foo.Bar
state.DoString("import ('Foo.Bar')");
// you should be able to access it in Service.SomeProperty
croizat
croizatOP•5w ago
well really not sure how the current service class is unsafe. If I do a simple
class ServiceWrapper
{
public IPlayer Player => Svc.Player;
}
...
var service = new ServiceWrapper();
state["Service"] = service;
class ServiceWrapper
{
public IPlayer Player => Svc.Player;
}
...
var service = new ServiceWrapper();
state["Service"] = service;
// fails by second iteration
local pos = Service.Player.Position
while count < 10 do
pos = Service.Player.Position
Game.LogInfo(pos)
count = count + 1
if count < 10 then
yield("/wait 5")
end
end

// works
Game.LogInfo(Service.Player.Position)
// fails by second iteration
local pos = Service.Player.Position
while count < 10 do
pos = Service.Player.Position
Game.LogInfo(pos)
count = count + 1
if count < 10 then
yield("/wait 5")
end
end

// works
Game.LogInfo(Service.Player.Position)
It results in the same as the last message where it can be accessed once, but multiple lines (like in a loop), it causes a crash. I was thinking maybe it's a speed limitation of the CLR access in nlua but it doesn't matter how fast or slow I access it a second time
Kiel
Kiel•5w ago
yeah I'm not sure why accessing the property throws a second time. NLua docs are sparse as you mentioned...I saw they have a wiki but the TOC is like...to articles that are imaginary lol. If you are not super tightly coupled to NLua already, give Laylua a try. it's certainly much newer and more experimental than the other "big" C#/Lua interop libraries, but I've been using it for my Discord bot for custom commands for almost a year and it's been working great. The dev has been very helpful at answering questions directly in the help server. And it is generally "crash" proof as the sandbox is designed to not crash the entire app if something happens in Lua, which I believe is a notable difference from some other libs. I've had a pretty simple experience interfacing with C# types in lua, though maybe my solution isn't applicable to every situation (or yours). I just added a little abstraction which my lua-facing types inherit from and then the class would be properly passed into the sandbox as userdata
croizat
croizatOP•5w ago
I might give it a try. Not super tied to NLua specifically, it's just what my program used before this rewrite so I'm most familiar with it, though that's not saying much Well seems like this may be a solvable problem. The crashing was apparently unrelated but it does throw an error. I can access it multiple times, but only if there's no waiting in between, so e.g. on the same frame where it's not helpful to access it multiple times. If there's a wait, then the properties are all nulled, but only for the imported class. Regular functions work fine after a wait
Kiel
Kiel•5w ago
i'm gonna guess that coroutines are incompatible with how NLua copies C# data into it so it's likely that maybe it's somehow on another thread where those properties are no longer accessible just a guess, though. see if you can find another way to "wait" instead of using yield

Did you find this page helpful?