Deep dive into UserData registration, descriptors, and custom types
UserData is the core mechanism in SolarSharp for exposing CLR objects to Lua scripts. Understanding how UserData works is essential for effective interoperability.
In Lua, userdata is a type that represents arbitrary C data. SolarSharp extends this concept to represent any CLR object, allowing Lua scripts to interact with .NET types seamlessly.The UserData class maintains a global registry of type descriptors that define how Lua can interact with CLR types.
Register all types marked with [SolarSharpUserData]:
[SolarSharpUserData]public class GameEntity{ public string Name { get; set; }}[SolarSharpUserData(AccessMode = InteropAccessMode.Preoptimized)]public class Player : GameEntity{ public int Score { get; set; }}// Register all marked typesUserData.RegisterAssembly();// Or from specific assemblyUserData.RegisterAssembly(typeof(GameEntity).Assembly);// Include extension typesUserData.RegisterAssembly(includeExtensionTypes: true);
The [SolarSharpUserData] attribute allows specifying the AccessMode property to control optimization behavior.
// Get descriptor for a typeIUserDataDescriptor descriptor = UserData.GetDescriptorForType<MyClass>(searchInterfaces: true);// Non-generic versionIUserDataDescriptor descriptor = UserData.GetDescriptorForType(typeof(MyClass), searchInterfaces: true);// Get descriptor for an object instancevar obj = new MyClass();IUserDataDescriptor descriptor = UserData.GetDescriptorForObject(obj);
The searchInterfaces parameter determines whether to search implemented interfaces if no exact match is found.
For types that need custom behavior, implement IUserDataType to become “self-describing”:
public class SmartObject : IUserDataType{ private Dictionary<string, object> _properties = new(); public LuaValue Index(Script script, LuaValue index, bool isDirectIndexing) { if (index.Type == DataType.String) { string key = index.String; if (_properties.TryGetValue(key, out object value)) { return LuaValue.FromObject(script, value); } } return LuaValue.Nil; } public bool SetIndex(Script script, LuaValue index, LuaValue value, bool isDirectIndexing) { if (index.Type == DataType.String) { string key = index.String; _properties[key] = value.ToObject(); return true; } return false; } public LuaValue MetaIndex(Script script, string metaname) { // Support custom metamethods if (metaname == "__len") { return LuaValue.NewNumber(_properties.Count); } return null; }}// Register with NoReflectionAllowed to use IUserDataType implementationUserData.RegisterType<SmartObject>(InteropAccessMode.NoReflectionAllowed);
Types implementing IUserDataType should be registered with InteropAccessMode.NoReflectionAllowed to prevent automatic descriptor generation from conflicting with the custom implementation.
Proxy types allow wrapping one type with another for Lua interop:
public class Vector3Proxy{ private Vector3 _vector; public Vector3Proxy(Vector3 vec) { _vector = vec; } public float X => _vector.X; public float Y => _vector.Y; public float Z => _vector.Z; public float Length() => _vector.Length();}// Register proxy with delegateUserData.RegisterProxyType<Vector3Proxy, Vector3>( vec => new Vector3Proxy(vec), InteropAccessMode.LazyOptimized);
// Check if exact type is registeredif (UserData.IsTypeRegistered<MyClass>()){ Console.WriteLine("MyClass is registered");}if (UserData.IsTypeRegistered(typeof(MyClass))){ Console.WriteLine("Type is registered");}// Get all registered typesIEnumerable<Type> types = UserData.GetRegisteredTypes();foreach (Type type in types){ Console.WriteLine($"Registered: {type.Name}");}// Include historical data (includes unregistered types)IEnumerable<Type> allTypes = UserData.GetRegisteredTypes(useHistoricalData: true);
Register extension methods to add functionality to existing types:
public static class ListExtensions{ public static T GetRandom<T>(this List<T> list) { return list[Random.Shared.Next(list.Count)]; } public static void Shuffle<T>(this List<T> list) { int n = list.Count; while (n > 1) { int k = Random.Shared.Next(n--); (list[n], list[k]) = (list[k], list[n]); } }}// Register extension typeUserData.RegisterExtensionType(typeof(ListExtensions));// Now List<T> instances have these methods in Luascript.Globals["numbers"] = new List<int> { 1, 2, 3, 4, 5 };script.DoString(@" numbers:Shuffle() local random = numbers:GetRandom() print(random)");
Extension methods are registered globally and apply to all instances of the extended type across all scripts.