Browse Source

Merge pull request #39 from AnnulusGames/source-generator

Add: Lua.SourceGenerator
Annulus Games 1 year ago
parent
commit
934fecab4d

+ 7 - 0
Lua.sln

@@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "sandbox\Cons
 EndProject
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmark", "sandbox\Benchmark\Benchmark.csproj", "{FC157C29-8AAE-49C8-9536-208E3F0698DA}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmark", "sandbox\Benchmark\Benchmark.csproj", "{FC157C29-8AAE-49C8-9536-208E3F0698DA}"
 EndProject
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lua.SourceGenerator", "src\Lua.SourceGenerator\Lua.SourceGenerator.csproj", "{C4BB264C-4D37-4E2D-99FD-4918CE22D7E4}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Debug|Any CPU = Debug|Any CPU
@@ -42,11 +44,16 @@ Global
 		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Release|Any CPU.Build.0 = Release|Any CPU
 		{FC157C29-8AAE-49C8-9536-208E3F0698DA}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C4BB264C-4D37-4E2D-99FD-4918CE22D7E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{C4BB264C-4D37-4E2D-99FD-4918CE22D7E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C4BB264C-4D37-4E2D-99FD-4918CE22D7E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{C4BB264C-4D37-4E2D-99FD-4918CE22D7E4}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(NestedProjects) = preSolution
 	GlobalSection(NestedProjects) = preSolution
 		{6E33BFBC-E51F-493E-9AF0-30C1100F5B5D} = {18A64E25-9557-457B-80AE-A6EFE853118D}
 		{6E33BFBC-E51F-493E-9AF0-30C1100F5B5D} = {18A64E25-9557-457B-80AE-A6EFE853118D}
 		{7572B7BC-FC73-42F0-B4F7-DA291B4EDB36} = {79458370-DD8A-48A4-B11E-8DF631520E8C}
 		{7572B7BC-FC73-42F0-B4F7-DA291B4EDB36} = {79458370-DD8A-48A4-B11E-8DF631520E8C}
 		{718A361C-AAF3-45A4-84D4-8C4FB6BB374E} = {33883F28-679F-48AD-8E64-3515C7BDAF5A}
 		{718A361C-AAF3-45A4-84D4-8C4FB6BB374E} = {33883F28-679F-48AD-8E64-3515C7BDAF5A}
 		{FC157C29-8AAE-49C8-9536-208E3F0698DA} = {33883F28-679F-48AD-8E64-3515C7BDAF5A}
 		{FC157C29-8AAE-49C8-9536-208E3F0698DA} = {33883F28-679F-48AD-8E64-3515C7BDAF5A}
+		{C4BB264C-4D37-4E2D-99FD-4918CE22D7E4} = {18A64E25-9557-457B-80AE-A6EFE853118D}
 	EndGlobalSection
 	EndGlobalSection
 EndGlobal
 EndGlobal

+ 7 - 0
sandbox/ConsoleApp1/ConsoleApp1.csproj

@@ -2,6 +2,10 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Lua\Lua.csproj" />
     <ProjectReference Include="..\..\src\Lua\Lua.csproj" />
+    <ProjectReference Include="..\..\src\Lua.SourceGenerator\Lua.SourceGenerator.csproj">
+			<OutputItemType>Analyzer</OutputItemType>
+			<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+		</ProjectReference>
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
@@ -9,6 +13,9 @@
     <TargetFramework>net8.0</TargetFramework>
     <TargetFramework>net8.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
+
+    <EmitCompilerGeneratedFiles>false</EmitCompilerGeneratedFiles>
+    <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
   </PropertyGroup>
   </PropertyGroup>
 
 
 </Project>
 </Project>

+ 43 - 0
sandbox/ConsoleApp1/LVec3.cs

@@ -0,0 +1,43 @@
+using System.Numerics;
+using Lua;
+
+[LuaObject]
+public partial class LVec3
+{
+    Vector3 value;
+
+    [LuaMember("x")]
+    public float X
+    {
+        get => value.X;
+        set => this.value = this.value with { X = value };
+    }
+
+    [LuaMember("y")]
+    public float Y
+    {
+        get => value.Y;
+        set => this.value = this.value with { Y = value };
+    }
+
+    [LuaMember("z")]
+    public float Z
+    {
+        get => value.Z;
+        set => this.value = this.value with { Z = value };
+    }
+
+    [LuaMember("create")]
+    public static LVec3 Create(float x, float y, float z)
+    {
+        return new LVec3()
+        {
+            value = new Vector3(x, y, z)
+        };
+    }
+
+    public override string ToString()
+    {
+        return value.ToString();
+    }
+}

+ 2 - 0
sandbox/ConsoleApp1/Program.cs

@@ -7,6 +7,8 @@ using Lua.Standard;
 var state = LuaState.Create();
 var state = LuaState.Create();
 state.OpenStandardLibraries();
 state.OpenStandardLibraries();
 
 
+state.Environment["vec3"] = new LVec3();
+
 try
 try
 {
 {
     var source = File.ReadAllText("test.lua");
     var source = File.ReadAllText("test.lua");

+ 0 - 58
sandbox/ConsoleApp1/vec3.lua

@@ -1,58 +0,0 @@
-Vec3 = {
-    new = function(x, y, z)
-        local instance = { x, y, z }
-        setmetatable(instance, Vec3)
-        return instance
-    end,
-
-    magnitude = function(self)
-        return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
-    end,
-
-    __index = function(table, index)
-        if index == 'x' then
-            return rawget(table, 1)
-        elseif index == 'y' then
-            return rawget(table, 2)
-        elseif index == 'z' then
-            return rawget(table, 3)
-        else
-            error('vec3 key must be x, y or z')
-        end
-    end,
-
-    __newindex = function(table, index, value)
-        if index == 'x' then
-            return rawset(table, 1, value)
-        elseif index == 'y' then
-            return rawset(table, 2, value)
-        elseif index == 'z' then
-            return rawset(table, 3, value)
-        else
-            error('vec3 key must be x, y or z')
-        end
-    end,
-
-    __add = function(a, b)
-        return Vec3.new(a.x + b.x, a.y + b.y, a.z + b.z)
-    end,
-
-    __sub = function(a, b)
-        return Vec3.new(a.x - b.x, a.y - b.y, a.z - b.z)
-    end,
-
-    __unm = function(a)
-        return Vec3.new(-a.x, -a.y, -a.z)
-    end,
-
-    __eq = function(a, b)
-        return a.x == b.y and a.y == b.y and a.z == b.z
-    end,
-
-    __tostring = function(self)
-        return '(' .. self.x .. ',' .. self.y .. ',' .. self.z .. ')'
-    end
-}
-
-local a = Vec3.new(1, 1, 1)
-local b = Vec3.new(1, 1, 1)

+ 18 - 0
src/Lua.SourceGenerator/Comparer.cs

@@ -0,0 +1,18 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+internal sealed class Comparer : IEqualityComparer<(GeneratorAttributeSyntaxContext, Compilation)>
+{
+    public static readonly Comparer Instance = new();
+
+    public bool Equals((GeneratorAttributeSyntaxContext, Compilation) x, (GeneratorAttributeSyntaxContext, Compilation) y)
+    {
+        return x.Item1.TargetNode.Equals(y.Item1.TargetNode);
+    }
+
+    public int GetHashCode((GeneratorAttributeSyntaxContext, Compilation) obj)
+    {
+        return obj.Item1.TargetNode.GetHashCode();
+    }
+}

+ 64 - 0
src/Lua.SourceGenerator/DiagnosticDescriptors.cs

@@ -0,0 +1,64 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+public static class DiagnosticDescriptors
+{
+    const string Category = "Lua";
+
+    public static readonly DiagnosticDescriptor MustBePartial = new(
+        id: "LUACS001",
+        title: "LuaObject type must be partial.",
+        category: Category,
+        messageFormat: "LuaObject type '{0}' must be partial",
+        defaultSeverity: DiagnosticSeverity.Error,
+        isEnabledByDefault: true);
+
+    public static readonly DiagnosticDescriptor NestedNotAllowed = new(
+        id: "LUACS002",
+        title: "LuaObject type must not be nested",
+        messageFormat: "LuaObject type '{0}' must be not nested",
+        category: Category,
+        defaultSeverity: DiagnosticSeverity.Error,
+        isEnabledByDefault: true);
+
+    public static readonly DiagnosticDescriptor AbstractNotAllowed = new(
+        id: "LUACS003",
+        title: "LuaObject type must not abstract",
+        messageFormat: "LuaObject object '{0}' must be not abstract",
+        category: Category,
+        defaultSeverity: DiagnosticSeverity.Error,
+        isEnabledByDefault: true);
+
+    public static readonly DiagnosticDescriptor InvalidPropertyType = new(
+        id: "LUACS004",
+        title: "The type of the field or property must be LuaValue or a type that can be converted to LuaValue.",
+        messageFormat: "The type of '{0}' must be LuaValue or a type that can be converted to LuaValue.",
+        category: Category,
+        defaultSeverity: DiagnosticSeverity.Error,
+        isEnabledByDefault: true);
+
+    public static readonly DiagnosticDescriptor InvalidReturnType = new(
+        id: "LUACS005",
+        title: "The return type must be LuaValue or types that can be converted to LuaValue.",
+        messageFormat: "The return type '{0}' must be LuaValue or types that can be converted to LuaValue.",
+        category: Category,
+        defaultSeverity: DiagnosticSeverity.Error,
+        isEnabledByDefault: true);
+
+    public static readonly DiagnosticDescriptor InvalidParameterType = new(
+        id: "LUACS006",
+        title: "The parameters must be LuaValue or types that can be converted to LuaValue.",
+        messageFormat: "The parameter '{0}' must be LuaValue or types that can be converted to LuaValue.",
+        category: Category,
+        defaultSeverity: DiagnosticSeverity.Error,
+        isEnabledByDefault: true);
+
+    public static readonly DiagnosticDescriptor DuplicateMetamethod = new(
+        id: "LUACS007",
+        title: "The type already contains same metamethod.",
+        messageFormat: "Type '{0}' already contains a '{1}' metamethod.,",
+        category: Category,
+        defaultSeverity: DiagnosticSeverity.Error,
+        isEnabledByDefault: true);
+}

+ 23 - 0
src/Lua.SourceGenerator/Lua.SourceGenerator.csproj

@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <LangVersion>12</LangVersion>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <IsRoslynComponent>true</IsRoslynComponent>
+    <AnalyzerLanguage>cs</AnalyzerLanguage>
+    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
+    <IncludeBuildOutput>false</IncludeBuildOutput>
+    <DevelopmentDependency>true</DevelopmentDependency>
+    <IncludeSymbols>false</IncludeSymbols>
+    <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0" />
+	
+	  <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
+  </ItemGroup>
+
+</Project>

+ 445 - 0
src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs

@@ -0,0 +1,445 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Lua.SourceGenerator;
+
+partial class LuaObjectGenerator
+{
+    static bool TryEmit(TypeMetadata typeMetadata, CodeBuilder builder, SymbolReferences references, Compilation compilation, in SourceProductionContext context)
+    {
+        try
+        {
+            var error = false;
+
+            // must be partial
+            if (!typeMetadata.IsPartial())
+            {
+                context.ReportDiagnostic(Diagnostic.Create(
+                    DiagnosticDescriptors.MustBePartial,
+                    typeMetadata.Syntax.Identifier.GetLocation(),
+                    typeMetadata.Symbol.Name));
+                error = true;
+            }
+
+            // nested is not allowed
+            if (typeMetadata.IsNested())
+            {
+                context.ReportDiagnostic(Diagnostic.Create(
+                    DiagnosticDescriptors.NestedNotAllowed,
+                    typeMetadata.Syntax.Identifier.GetLocation(),
+                    typeMetadata.Symbol.Name));
+                error = true;
+            }
+
+            // verify abstract/interface
+            if (typeMetadata.Symbol.IsAbstract)
+            {
+                context.ReportDiagnostic(Diagnostic.Create(
+                    DiagnosticDescriptors.AbstractNotAllowed,
+                    typeMetadata.Syntax.Identifier.GetLocation(),
+                    typeMetadata.TypeName));
+                error = true;
+            }
+
+            if (!ValidateMembers(typeMetadata, compilation, references, context))
+            {
+                error = true;
+            }
+
+            if (error)
+            {
+                return false;
+            }
+
+            builder.AppendLine("// <auto-generated />");
+            builder.AppendLine("#nullable enable");
+            builder.AppendLine("#pragma warning disable CS0162 // Unreachable code");
+            builder.AppendLine("#pragma warning disable CS0219 // Variable assigned but never used");
+            builder.AppendLine("#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.");
+            builder.AppendLine("#pragma warning disable CS8601 // Possible null reference assignment");
+            builder.AppendLine("#pragma warning disable CS8602 // Possible null return");
+            builder.AppendLine("#pragma warning disable CS8604 // Possible null reference argument for parameter");
+            builder.AppendLine("#pragma warning disable CS8631 // The type cannot be used as type parameter in the generic type or method");
+            builder.AppendLine();
+
+            var ns = typeMetadata.Symbol.ContainingNamespace;
+            if (!ns.IsGlobalNamespace)
+            {
+                builder.AppendLine($"namespace {ns}");
+                builder.BeginBlock();
+            }
+
+            var typeDeclarationKeyword = (typeMetadata.Symbol.IsRecord, typeMetadata.Symbol.IsValueType) switch
+            {
+                (true, true) => "record struct",
+                (true, false) => "record",
+                (false, true) => "struct",
+                (false, false) => "class",
+            };
+
+            using var _ = builder.BeginBlockScope($"partial {typeDeclarationKeyword} {typeMetadata.TypeName} : global::Lua.ILuaUserData");
+
+            var metamethodSet = new HashSet<LuaObjectMetamethod>();
+
+            if (!TryEmitMethods(typeMetadata, builder, references, metamethodSet, context))
+            {
+                return false;
+            }
+
+            if (!TryEmitIndexMetamethod(typeMetadata, builder, context))
+            {
+                return false;
+            }
+
+            if (!TryEmitNewIndexMetamethod(typeMetadata, builder, context))
+            {
+                return false;
+            }
+
+            if (!TryEmitMetatable(builder, metamethodSet, context))
+            {
+                return false;
+            }
+
+            // implicit operator
+            builder.AppendLine($"public static implicit operator global::Lua.LuaValue({typeMetadata.FullTypeName} value)");
+            using (builder.BeginBlockScope())
+            {
+                builder.AppendLine("return new(value);");
+            }
+
+            if (!ns.IsGlobalNamespace) builder.EndBlock();
+
+            builder.AppendLine("#pragma warning restore CS0162 // Unreachable code");
+            builder.AppendLine("#pragma warning restore CS0219 // Variable assigned but never used");
+            builder.AppendLine("#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.");
+            builder.AppendLine("#pragma warning restore CS8601 // Possible null reference assignment");
+            builder.AppendLine("#pragma warning restore CS8602 // Possible null return");
+            builder.AppendLine("#pragma warning restore CS8604 // Possible null reference argument for parameter");
+            builder.AppendLine("#pragma warning restore CS8631 // The type cannot be used as type parameter in the generic type or method");
+            return true;
+        }
+        catch (Exception)
+        {
+            return false;
+        }
+    }
+
+    static bool ValidateMembers(TypeMetadata typeMetadata, Compilation compilation, SymbolReferences references, in SourceProductionContext context)
+    {
+        var isValid = true;
+
+        foreach (var property in typeMetadata.Properties)
+        {
+            if (SymbolEqualityComparer.Default.Equals(property.Type, references.LuaValue)) continue;
+            if (SymbolEqualityComparer.Default.Equals(property.Type, typeMetadata.Symbol)) continue;
+
+            var conversion = compilation.ClassifyConversion(property.Type, references.LuaValue);
+            if (!conversion.Exists)
+            {
+                context.ReportDiagnostic(Diagnostic.Create(
+                    DiagnosticDescriptors.InvalidPropertyType,
+                    property.Symbol.Locations.FirstOrDefault(),
+                    property.Type.Name));
+
+                isValid = false;
+            }
+        }
+
+        foreach (var method in typeMetadata.Methods)
+        {
+            if (!method.Symbol.ReturnsVoid)
+            {
+                var typeSymbol = method.Symbol.ReturnType;
+
+                if (method.IsAsync)
+                {
+                    var namedType = (INamedTypeSymbol)typeSymbol;
+                    if (namedType.TypeArguments.Length == 0) goto PARAMETERS;
+
+                    typeSymbol = namedType.TypeArguments[0];
+                }
+
+                if (SymbolEqualityComparer.Default.Equals(typeSymbol, references.LuaValue)) goto PARAMETERS;
+                if (SymbolEqualityComparer.Default.Equals(typeSymbol, typeMetadata.Symbol)) goto PARAMETERS;
+
+                var conversion = compilation.ClassifyConversion(typeSymbol, references.LuaValue);
+                if (!conversion.Exists)
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.InvalidReturnType,
+                        typeSymbol.Locations.FirstOrDefault(),
+                        typeSymbol.Name));
+
+                    isValid = false;
+                }
+            }
+
+        PARAMETERS:
+            foreach (var typeSymbol in method.Symbol.Parameters
+                .Select(x => x.Type))
+            {
+                if (SymbolEqualityComparer.Default.Equals(typeSymbol, references.LuaValue)) continue;
+                if (SymbolEqualityComparer.Default.Equals(typeSymbol, typeMetadata.Symbol)) continue;
+
+                var conversion = compilation.ClassifyConversion(typeSymbol, references.LuaValue);
+                if (!conversion.Exists)
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.InvalidParameterType,
+                        typeSymbol.Locations.FirstOrDefault(),
+                        typeSymbol.Name));
+
+                    isValid = false;
+                }
+            }
+        }
+
+        return isValid;
+    }
+
+    static bool TryEmitIndexMetamethod(TypeMetadata typeMetadata, CodeBuilder builder, in SourceProductionContext context)
+    {
+        builder.AppendLine("static readonly global::Lua.LuaFunction __metamethod_index = new global::Lua.LuaFunction((context, buffer, ct) =>");
+
+        using (builder.BeginBlockScope())
+        {
+            builder.AppendLine($"var userData = context.GetArgument<{typeMetadata.FullTypeName}>(0);");
+            builder.AppendLine($"var key = context.GetArgument<global::System.String>(1);");
+            builder.AppendLine("var result = key switch");
+
+            using (builder.BeginBlockScope())
+            {
+                foreach (var propertyMetadata in typeMetadata.Properties)
+                {
+                    if (propertyMetadata.IsStatic)
+                    {
+                        builder.AppendLine(@$"""{propertyMetadata.LuaMemberName}"" => new global::Lua.LuaValue({typeMetadata.FullTypeName}.{propertyMetadata.Symbol.Name}),");
+                    }
+                    else
+                    {
+                        builder.AppendLine(@$"""{propertyMetadata.LuaMemberName}"" => new global::Lua.LuaValue(userData.{propertyMetadata.Symbol.Name}),");
+                    }
+                }
+
+                foreach (var methodMetadata in typeMetadata.Methods
+                    .Where(x => x.HasMemberAttribute))
+                {
+                    builder.AppendLine(@$"""{methodMetadata.LuaMemberName}"" => new global::Lua.LuaValue(__function_{methodMetadata.LuaMemberName}),");
+                }
+
+                builder.AppendLine(@$"_ => global::Lua.LuaValue.Nil,");
+            }
+            builder.AppendLine(";");
+
+            builder.AppendLine("buffer.Span[0] = result;");
+            builder.AppendLine("return new(1);");
+        }
+
+        builder.AppendLine(");");
+
+        return true;
+    }
+
+    static bool TryEmitNewIndexMetamethod(TypeMetadata typeMetadata, CodeBuilder builder, in SourceProductionContext context)
+    {
+        builder.AppendLine("static readonly global::Lua.LuaFunction __metamethod_newindex = new global::Lua.LuaFunction((context, buffer, ct) =>");
+
+        using (builder.BeginBlockScope())
+        {
+            builder.AppendLine($"var userData = context.GetArgument<{typeMetadata.FullTypeName}>(0);");
+            builder.AppendLine($"var key = context.GetArgument<global::System.String>(1);");
+            builder.AppendLine("switch (key)");
+
+            using (builder.BeginBlockScope())
+            {
+                foreach (var propertyMetadata in typeMetadata.Properties)
+                {
+                    builder.AppendLine(@$"case ""{propertyMetadata.LuaMemberName}"":");
+
+                    using (builder.BeginIndentScope())
+                    {
+                        if (propertyMetadata.IsReadOnly)
+                        {
+                            builder.AppendLine($@"throw new global::Lua.LuaRuntimeException(context.State.GetTraceback(), $""'{{key}}' cannot overwrite."");");
+                        }
+                        else if (propertyMetadata.IsStatic)
+                        {
+                            builder.AppendLine(@$"{typeMetadata.FullTypeName}.{propertyMetadata.Symbol.Name} = context.GetArgument<{propertyMetadata.TypeFullName}>(2);");
+                            builder.AppendLine("break;");
+                        }
+                        else
+                        {
+                            builder.AppendLine(@$"userData.{propertyMetadata.Symbol.Name} = context.GetArgument<{propertyMetadata.TypeFullName}>(2);");
+                            builder.AppendLine("break;");
+                        }
+                    }
+                }
+
+                foreach (var methodMetadata in typeMetadata.Methods
+                    .Where(x => x.HasMemberAttribute))
+                {
+                    builder.AppendLine(@$"case ""{methodMetadata.LuaMemberName}"":");
+
+                    using (builder.BeginIndentScope())
+                    {
+                        builder.AppendLine($@"throw new global::Lua.LuaRuntimeException(context.State.GetTraceback(), $""'{{key}}' cannot overwrite."");");
+                    }
+                }
+
+                builder.AppendLine(@$"default:");
+
+                using (builder.BeginIndentScope())
+                {
+                    builder.AppendLine(@$"throw new global::Lua.LuaRuntimeException(context.State.GetTraceback(), $""'{{key}}'  not found."");");
+                }
+            }
+
+            builder.AppendLine("return new(0);");
+        }
+
+        builder.AppendLine(");");
+
+        return true;
+    }
+
+    static bool TryEmitMethods(TypeMetadata typeMetadata, CodeBuilder builder, SymbolReferences references, HashSet<LuaObjectMetamethod> metamethodSet, in SourceProductionContext context)
+    {
+        builder.AppendLine();
+
+        foreach (var methodMetadata in typeMetadata.Methods)
+        {
+            string? functionName = null;
+
+            if (methodMetadata.HasMemberAttribute)
+            {
+                functionName = $"__function_{methodMetadata.LuaMemberName}";
+                EmitMethodFunction(functionName, typeMetadata, methodMetadata, builder, references);
+            }
+
+            if (methodMetadata.HasMetamethodAttribute)
+            {
+                if (!metamethodSet.Add(methodMetadata.Metamethod))
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.DuplicateMetamethod,
+                        methodMetadata.Symbol.Locations.FirstOrDefault(),
+                        typeMetadata.TypeName,
+                        methodMetadata.Metamethod
+                    ));
+
+                    continue;
+                }
+
+                if (functionName == null)
+                {
+                    EmitMethodFunction($"__metamethod_{methodMetadata.Metamethod}", typeMetadata, methodMetadata, builder, references);
+                }
+                else
+                {
+                    builder.AppendLine($"static global::Lua.LuaFunction __metamethod_{methodMetadata.Metamethod} => {functionName};");
+                }
+            }
+        }
+
+        return true;
+    }
+
+    static void EmitMethodFunction(string functionName, TypeMetadata typeMetadata, MethodMetadata methodMetadata, CodeBuilder builder, SymbolReferences references)
+    {
+        builder.AppendLine($"static readonly global::Lua.LuaFunction {functionName} = new global::Lua.LuaFunction({(methodMetadata.IsAsync ? "async" : "")} (context, buffer, ct) =>");
+
+        using (builder.BeginBlockScope())
+        {
+            var index = 0;
+
+            if (!methodMetadata.IsStatic)
+            {
+                builder.AppendLine($"var userData = context.GetArgument<{typeMetadata.FullTypeName}>(0);");
+                index++;
+            }
+
+            foreach (var parameter in methodMetadata.Symbol.Parameters)
+            {
+                builder.AppendLine($"var arg{index} = context.GetArgument<{parameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>({index});");
+                index++;
+            }
+
+            if (methodMetadata.HasReturnValue)
+            {
+                builder.Append("var result = ");
+            }
+
+            if (methodMetadata.IsAsync)
+            {
+                builder.Append("await ", false);
+            }
+
+            if (methodMetadata.IsStatic)
+            {
+                builder.Append($"{typeMetadata.FullTypeName}.{methodMetadata.Symbol.Name}(", false);
+                builder.Append(string.Join(",", Enumerable.Range(0, index).Select(x => $"arg{x}")), false);
+                builder.AppendLine(");", false);
+            }
+            else
+            {
+                builder.Append($"userData.{methodMetadata.Symbol.Name}(");
+                builder.Append(string.Join(",", Enumerable.Range(1, index - 1).Select(x => $"arg{x}")), false);
+                builder.AppendLine(");", false);
+            }
+
+            if (methodMetadata.HasReturnValue)
+            {
+                if (SymbolEqualityComparer.Default.Equals(methodMetadata.Symbol.ReturnType, references.LuaValue))
+                {
+                    builder.AppendLine("buffer.Span[0] = result;");
+                }
+                else
+                {
+                    builder.AppendLine("buffer.Span[0] = new global::Lua.LuaValue(result);");
+                }
+                
+                builder.AppendLine($"return {(methodMetadata.IsAsync ? "1" : "new(1)")};");
+            }
+            else
+            {
+                builder.AppendLine($"return {(methodMetadata.IsAsync ? "0" : "new(0)")};");
+            }
+        }
+        builder.AppendLine(");");
+        builder.AppendLine();
+    }
+
+    static bool TryEmitMetatable(CodeBuilder builder, IEnumerable<LuaObjectMetamethod> metamethods, in SourceProductionContext context)
+    {
+        builder.AppendLine("global::Lua.LuaTable? global::Lua.ILuaUserData.Metatable");
+        using (builder.BeginBlockScope())
+        {
+            builder.AppendLine("get");
+            using (builder.BeginBlockScope())
+            {
+                builder.AppendLine("if (__metatable != null) return __metatable;");
+                builder.AppendLine();
+                builder.AppendLine("__metatable = new();");
+                builder.AppendLine("__metatable[global::Lua.Runtime.Metamethods.Index] = __metamethod_index;");
+                builder.AppendLine("__metatable[global::Lua.Runtime.Metamethods.NewIndex] = __metamethod_newindex;");
+                foreach (var metamethod in metamethods)
+                {
+                    builder.AppendLine($"__metatable[global::Lua.Runtime.Metamethods.{metamethod}] = __metamethod_{metamethod};");
+                }
+                builder.AppendLine("return __metatable;");
+            }
+
+            builder.AppendLine("set");
+            using (builder.BeginBlockScope())
+            {
+                builder.AppendLine("__metatable = value;");
+            }
+        }
+
+        builder.AppendLine("static global::Lua.LuaTable? __metatable;");
+        builder.AppendLine();
+
+        return true;
+    }
+}

+ 54 - 0
src/Lua.SourceGenerator/LuaObjectGenerator.cs

@@ -0,0 +1,54 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Lua.SourceGenerator;
+
+[Generator(LanguageNames.CSharp)]
+public partial class LuaObjectGenerator : IIncrementalGenerator
+{
+    public void Initialize(IncrementalGeneratorInitializationContext context)
+    {
+        var provider = context.SyntaxProvider
+            .ForAttributeWithMetadataName(
+                "Lua.LuaObjectAttribute",
+                static (node, cancellation) =>
+                {
+                    return node is ClassDeclarationSyntax
+                        or RecordDeclarationSyntax;
+                },
+                static (context, cancellation) => { return context; })
+            .Combine(context.CompilationProvider)
+            .WithComparer(Comparer.Instance);
+
+        context.RegisterSourceOutput(
+            context.CompilationProvider.Combine(provider.Collect()),
+            (sourceProductionContext, t) =>
+            {
+                var (compilation, list) = t;
+                var references = SymbolReferences.Create(compilation);
+                if (references == null) return;
+
+                var builder = new CodeBuilder();
+
+                var targetTypes = new List<TypeMetadata>();
+
+                foreach (var (x, _) in list)
+                {
+                    var typeMeta = new TypeMetadata((TypeDeclarationSyntax)x.TargetNode, (INamedTypeSymbol)x.TargetSymbol, references);
+
+                    if (TryEmit(typeMeta, builder, references, compilation, in sourceProductionContext))
+                    {
+                        var fullType = typeMeta.FullTypeName
+                            .Replace("global::", "")
+                            .Replace("<", "_")
+                            .Replace(">", "_");
+
+                        sourceProductionContext.AddSource($"{fullType}.LuaObject.g.cs", builder.ToString());
+                        targetTypes.Add(typeMeta);
+                    }
+
+                    builder.Clear();
+                }
+            });
+    }
+}

+ 23 - 0
src/Lua.SourceGenerator/LuaObjectMetamethod.cs

@@ -0,0 +1,23 @@
+namespace Lua.SourceGenerator;
+
+// same as Lua.LuaObjectMetamethod
+
+internal enum LuaObjectMetamethod
+{
+    Add,
+    Sub,
+    Mul,
+    Div,
+    Mod,
+    Pow,
+    Unm,
+    Len,
+    Eq,
+    Lt,
+    Le,
+    Call,
+    Concat,
+    Pairs,
+    IPairs,
+    ToString,
+}

+ 55 - 0
src/Lua.SourceGenerator/MethodMetadata.cs

@@ -0,0 +1,55 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+internal class MethodMetadata
+{
+    public IMethodSymbol Symbol { get; }
+    public bool IsStatic { get; }
+    public bool IsAsync { get; }
+    public bool HasReturnValue { get; }
+    public bool HasMemberAttribute { get; }
+    public bool HasMetamethodAttribute { get; }
+    public string LuaMemberName { get; }
+    public LuaObjectMetamethod Metamethod { get; }
+
+    public MethodMetadata(IMethodSymbol symbol, SymbolReferences references)
+    {
+        Symbol = symbol;
+        IsStatic = symbol.IsStatic;
+
+        var returnType = symbol.ReturnType;
+        var fullName = (returnType.ContainingNamespace.IsGlobalNamespace ? "" : (returnType.ContainingNamespace + ".")) + returnType.Name;
+        IsAsync = fullName is "System.Threading.Tasks.Task"
+            or "System.Threading.Tasks.ValueTask"
+            or "Cysharp.Threading.Tasks.UniTask"
+            or "UnityEngine.Awaitable";
+
+        HasReturnValue = !symbol.ReturnsVoid && !(IsAsync && returnType is INamedTypeSymbol n && !n.IsGenericType);
+
+        LuaMemberName = symbol.Name;
+
+        var memberAttribute = symbol.GetAttribute(references.LuaMemberAttribute);
+        HasMemberAttribute = memberAttribute != null;
+
+        if (memberAttribute != null)
+        {
+            if (memberAttribute.ConstructorArguments.Length > 0)
+            {
+                var value = memberAttribute.ConstructorArguments[0].Value;
+                if (value is string str)
+                {
+                    LuaMemberName = str;
+                }
+            }
+        }
+
+        var metamethodAttribute = symbol.GetAttribute(references.LuaMetamethodAttribute);
+        HasMetamethodAttribute = metamethodAttribute != null;
+
+        if (metamethodAttribute != null)
+        {
+            Metamethod = (LuaObjectMetamethod)Enum.Parse(typeof(LuaObjectMetamethod), metamethodAttribute.ConstructorArguments[0].Value!.ToString());
+        }
+    }
+}

+ 53 - 0
src/Lua.SourceGenerator/PropertyMetadata.cs

@@ -0,0 +1,53 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+public class PropertyMetadata
+{
+    public ISymbol Symbol { get; }
+    public ITypeSymbol Type { get; }
+    public string TypeFullName { get; }
+    public bool IsStatic { get; }
+    public bool IsReadOnly { get; }
+    public string LuaMemberName { get; }
+
+    public PropertyMetadata(ISymbol symbol, SymbolReferences references)
+    {
+        Symbol = symbol;
+
+        IsStatic = symbol.IsStatic;
+
+        if (symbol is IFieldSymbol field)
+        {
+            Type = field.Type;
+            TypeFullName = field.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+            IsReadOnly = field.IsReadOnly;
+        }
+        else if (symbol is IPropertySymbol property)
+        {
+            Type = property.Type;
+            TypeFullName = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+            IsReadOnly = property.SetMethod == null;
+        }
+        else
+        {
+            Type = default!;
+            TypeFullName = "";
+        }
+
+        LuaMemberName = symbol.Name;
+
+        var memberAttribute = symbol.GetAttribute(references.LuaMemberAttribute);
+        if (memberAttribute != null)
+        {
+            if (memberAttribute.ConstructorArguments.Length > 0)
+            {
+                var value = memberAttribute.ConstructorArguments[0].Value;
+                if (value is string str)
+                {
+                    LuaMemberName = str;
+                }
+            }
+        }
+    }
+}

+ 27 - 0
src/Lua.SourceGenerator/SymbolReferences.cs

@@ -0,0 +1,27 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+public sealed class SymbolReferences
+{
+    public static SymbolReferences? Create(Compilation compilation)
+    {
+        var luaObjectAttribute = compilation.GetTypeByMetadataName("Lua.LuaObjectAttribute");
+        if (luaObjectAttribute == null) return null;
+
+        return new SymbolReferences
+        {
+            LuaObjectAttribute = luaObjectAttribute,
+            LuaMemberAttribute = compilation.GetTypeByMetadataName("Lua.LuaMemberAttribute")!,
+            LuaIgnoreMemberAttribute = compilation.GetTypeByMetadataName("Lua.LuaIgnoreMemberAttribute")!,
+            LuaMetamethodAttribute = compilation.GetTypeByMetadataName("Lua.LuaMetamethodAttribute")!,
+            LuaValue = compilation.GetTypeByMetadataName("Lua.LuaValue")!,
+        };
+    }
+
+    public INamedTypeSymbol LuaObjectAttribute { get; private set; } = default!;
+    public INamedTypeSymbol LuaMemberAttribute { get; private set; } = default!;
+    public INamedTypeSymbol LuaIgnoreMemberAttribute { get; private set; } = default!;
+    public INamedTypeSymbol LuaMetamethodAttribute { get; private set; } = default!;
+    public INamedTypeSymbol LuaValue { get; private set; } = default!;
+}

+ 63 - 0
src/Lua.SourceGenerator/TypeMetadata.cs

@@ -0,0 +1,63 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Lua.SourceGenerator;
+
+internal class TypeMetadata
+{
+    public TypeDeclarationSyntax Syntax { get; }
+    public INamedTypeSymbol Symbol { get; }
+    public string TypeName { get; }
+    public string FullTypeName { get; }
+    public PropertyMetadata[] Properties { get; }
+    public MethodMetadata[] Methods { get; }
+
+    public TypeMetadata(TypeDeclarationSyntax syntax, INamedTypeSymbol symbol, SymbolReferences references)
+    {
+        Syntax = syntax;
+        Symbol = symbol;
+
+        TypeName = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
+        FullTypeName = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+        Properties = Symbol.GetAllMembers(false)
+            .Where(x => x is (IFieldSymbol or IPropertySymbol) and { IsImplicitlyDeclared: false })
+            .Where(x =>
+            {
+                if (!x.ContainsAttribute(references.LuaMemberAttribute)) return false;
+                if (x.ContainsAttribute(references.LuaIgnoreMemberAttribute)) return false;
+
+                if (x is IPropertySymbol p)
+                {
+                    if (p.GetMethod == null || p.SetMethod == null) return false;
+                    if (p.IsIndexer) return false;
+                }
+
+                return true;
+            })
+            .Select(x => new PropertyMetadata(x, references))
+            .ToArray();
+
+        Methods = Symbol.GetAllMembers(false)
+            .Where(x => x is IMethodSymbol and { IsImplicitlyDeclared: false })
+            .Select(x => (IMethodSymbol)x)
+            .Where(x =>
+            {
+                if (x.ContainsAttribute(references.LuaIgnoreMemberAttribute)) return false;
+                return x.ContainsAttribute(references.LuaMemberAttribute) || x.ContainsAttribute(references.LuaMetamethodAttribute);
+            })
+            .Select(x => new MethodMetadata(x, references))
+            .ToArray();
+    }
+
+    public bool IsPartial()
+    {
+        return Syntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
+    }
+
+    public bool IsNested()
+    {
+        return Syntax.Parent is TypeDeclarationSyntax;
+    }
+}

+ 120 - 0
src/Lua.SourceGenerator/Utilities/CodeBuilder.cs

@@ -0,0 +1,120 @@
+using System.Text;
+
+namespace Lua.SourceGenerator;
+
+internal sealed class CodeBuilder
+{
+    public ref struct IndentScope
+    {
+        readonly CodeBuilder source;
+
+        public IndentScope(CodeBuilder source, string? startLine = null)
+        {
+            this.source = source;
+            source.AppendLine(startLine);
+            source.IncreaseIndent();
+        }
+
+        public void Dispose()
+        {
+            source.DecreaseIndent();
+        }
+    }
+
+    public ref struct BlockScope
+    {
+        readonly CodeBuilder source;
+
+        public BlockScope(CodeBuilder source, string? startLine = null)
+        {
+            this.source = source;
+            source.AppendLine(startLine);
+            source.BeginBlock();
+        }
+
+        public void Dispose()
+        {
+            source.EndBlock();
+        }
+    }
+
+    readonly StringBuilder buffer = new();
+    int indentLevel;
+
+    public IndentScope BeginIndentScope(string? startLine = null) => new(this, startLine);
+    public BlockScope BeginBlockScope(string? startLine = null) => new(this, startLine);
+
+    public void Append(string value, bool indent = true)
+    {
+        if (indent)
+        {
+            buffer.Append($"{new string(' ', indentLevel * 4)} {value}");
+        }
+        else
+        {
+            buffer.Append(value);
+        }
+    }
+
+    public void AppendLine(string? value = null, bool indent = true)
+    {
+        if (string.IsNullOrEmpty(value))
+        {
+            buffer.AppendLine();
+        }
+        else if (indent)
+        {
+            buffer.AppendLine($"{new string(' ', indentLevel * 4)} {value}");
+        }
+        else
+        {
+            buffer.AppendLine(value);
+        }
+    }
+
+    public void AppendByteArrayString(byte[] bytes)
+    {
+        buffer.Append("{ ");
+        var first = true;
+        foreach (var x in bytes)
+        {
+            if (!first)
+            {
+                buffer.Append(", ");
+            }
+            buffer.Append(x);
+            first = false;
+        }
+        buffer.Append(" }");
+    }
+
+    public override string ToString() => buffer.ToString();
+
+    public void IncreaseIndent()
+    {
+        indentLevel++;
+    }
+
+    public void DecreaseIndent()
+    {
+        if (indentLevel > 0)
+            indentLevel--;
+    }
+
+    public void BeginBlock()
+    {
+        AppendLine("{");
+        IncreaseIndent();
+    }
+
+    public void EndBlock()
+    {
+        DecreaseIndent();
+        AppendLine("}");
+    }
+
+    public void Clear()
+    {
+        buffer.Clear();
+    }
+}

+ 20 - 0
src/Lua.SourceGenerator/Utilities/RoslynAnalyzerExtensions.cs

@@ -0,0 +1,20 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+internal static class RoslynAnalyzerExtensions
+{
+    public static AttributeData? FindAttribute(this IEnumerable<AttributeData> attributeDataList, string typeName)
+    {
+        return attributeDataList
+            .Where(x => x.AttributeClass?.ToDisplayString() == typeName)
+            .FirstOrDefault();
+    }
+
+    public static AttributeData? FindAttributeShortName(this IEnumerable<AttributeData> attributeDataList, string typeName)
+    {
+        return attributeDataList
+            .Where(x => x.AttributeClass?.Name == typeName)
+            .FirstOrDefault();
+    }
+}

+ 40 - 0
src/Lua.SourceGenerator/Utilities/SymbolExtensions.cs

@@ -0,0 +1,40 @@
+using Microsoft.CodeAnalysis;
+
+namespace Lua.SourceGenerator;
+
+internal static class SymbolExtensions
+{
+    public static bool ContainsAttribute(this ISymbol symbol, INamedTypeSymbol attribtue)
+    {
+        return symbol.GetAttributes().Any(x => SymbolEqualityComparer.Default.Equals(x.AttributeClass, attribtue));
+    }
+
+    public static AttributeData? GetAttribute(this ISymbol symbol, INamedTypeSymbol attribtue)
+    {
+        return symbol.GetAttributes().FirstOrDefault(x => SymbolEqualityComparer.Default.Equals(x.AttributeClass, attribtue));
+    }
+
+    public static IEnumerable<ISymbol> GetAllMembers(this INamedTypeSymbol symbol, bool withoutOverride = true)
+    {
+        // Iterate Parent -> Derived
+        if (symbol.BaseType != null)
+        {
+            foreach (var item in GetAllMembers(symbol.BaseType))
+            {
+                // override item already iterated in parent type
+                if (!withoutOverride || !item.IsOverride)
+                {
+                    yield return item;
+                }
+            }
+        }
+
+        foreach (var item in symbol.GetMembers())
+        {
+            if (!withoutOverride || !item.IsOverride)
+            {
+                yield return item;
+            }
+        }
+    }
+}

+ 42 - 0
src/Lua/Attributes.cs

@@ -0,0 +1,42 @@
+namespace Lua;
+
+[AttributeUsage(AttributeTargets.Class)]
+public sealed class LuaObjectAttribute : Attribute
+{
+    public LuaObjectAttribute()
+    {
+    }
+
+    public LuaObjectAttribute(string name)
+    {
+        Name = name;
+    }
+
+    public string? Name { get; }
+}
+
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)]
+public sealed class LuaMemberAttribute : Attribute
+{
+    public LuaMemberAttribute()
+    {
+    }
+
+    public LuaMemberAttribute(string name)
+    {
+        Name = name;
+    }
+
+    public string? Name { get; }
+}
+
+[AttributeUsage(AttributeTargets.Method)]
+public sealed class LuaMetamethodAttribute(LuaObjectMetamethod metamethod) : Attribute
+{
+    public LuaObjectMetamethod Metamethod { get; } = metamethod;
+}
+
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)]
+public sealed class LuaIgnoreMemberAttribute : Attribute
+{
+}

+ 12 - 1
src/Lua/Lua.csproj

@@ -18,6 +18,11 @@
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
     </PackageReference>
     </PackageReference>
+
+    <None Include="..\Lua.SourceGenerator\bin\$(Configuration)\netstandard2.0\Lua.SourceGenerator.dll"
+          PackagePath="analyzers\dotnet\cs"
+          Pack="true"
+          Visible="false" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup Condition="$(TargetFramework) == 'netstandard2.1'">
   <ItemGroup Condition="$(TargetFramework) == 'netstandard2.1'">
@@ -26,8 +31,14 @@
   
   
   <ItemGroup>
   <ItemGroup>
       <None Include="../../Icon.png" Pack="true" PackagePath="/" />
       <None Include="../../Icon.png" Pack="true" PackagePath="/" />
-      <None Include="..\..\README.md" Pack="true" PackagePath="README.md"/>
+      <None Include="..\..\README.md" Pack="true" PackagePath="README.md" />
       <EmbeddedResource Include="..\..\LICENSE" />
       <EmbeddedResource Include="..\..\LICENSE" />
   </ItemGroup>
   </ItemGroup>
+  
+  <ItemGroup>
+    <ProjectReference Include="..\Lua.SourceGenerator\Lua.SourceGenerator.csproj"
+      OutputItemType="Analyzer"
+      ReferenceOutputAssembly="false"/>
+  </ItemGroup>
 
 
 </Project>
 </Project>

+ 21 - 0
src/Lua/LuaObjectMetamethod.cs

@@ -0,0 +1,21 @@
+namespace Lua;
+
+public enum LuaObjectMetamethod
+{
+    Add,
+    Sub,
+    Mul,
+    Div,
+    Mod,
+    Pow,
+    Unm,
+    Len,
+    Eq,
+    Lt,
+    Le,
+    Call,
+    Concat,
+    Pairs,
+    IPairs,
+    ToString,
+}

+ 2 - 2
src/Lua/LuaState.cs

@@ -109,7 +109,7 @@ public sealed class LuaState
             LuaValueType.Number => numberMetatable,
             LuaValueType.Number => numberMetatable,
             LuaValueType.Function => functionMetatable,
             LuaValueType.Function => functionMetatable,
             LuaValueType.Thread => threadMetatable,
             LuaValueType.Thread => threadMetatable,
-            LuaValueType.UserData => value.UnsafeRead<LuaUserData>().Metatable,
+            LuaValueType.UserData => value.UnsafeRead<ILuaUserData>().Metatable,
             LuaValueType.Table => value.UnsafeRead<LuaTable>().Metatable,
             LuaValueType.Table => value.UnsafeRead<LuaTable>().Metatable,
             _ => null
             _ => null
         };
         };
@@ -141,7 +141,7 @@ public sealed class LuaState
                 threadMetatable = metatable;
                 threadMetatable = metatable;
                 break;
                 break;
             case LuaValueType.UserData:
             case LuaValueType.UserData:
-                value.UnsafeRead<LuaUserData>().Metatable = metatable;
+                value.UnsafeRead<ILuaUserData>().Metatable = metatable;
                 break;
                 break;
             case LuaValueType.Table:
             case LuaValueType.Table:
                 value.UnsafeRead<LuaTable>().Metatable = metatable;
                 value.UnsafeRead<LuaTable>().Metatable = metatable;

+ 2 - 7
src/Lua/LuaUserData.cs

@@ -1,11 +1,6 @@
 namespace Lua;
 namespace Lua;
 
 
-public abstract class LuaUserData
+public interface ILuaUserData
 {
 {
-    public LuaTable? Metatable { get; set; }
-}
-
-public class LuaUserData<T>(T value) : LuaUserData
-{
-    public T Value { get; } = value;
+    LuaTable? Metatable { get; set; }
 }
 }

+ 2 - 7
src/Lua/LuaValue.cs

@@ -167,7 +167,7 @@ public readonly struct LuaValue : IEquatable<LuaValue>
                     break;
                     break;
                 }
                 }
             case LuaValueType.UserData:
             case LuaValueType.UserData:
-                if (t == typeof(LuaUserData) || t.IsSubclassOf(typeof(LuaUserData)))
+                if (t == typeof(ILuaUserData) || typeof(ILuaUserData).IsAssignableFrom(t))
                 {
                 {
                     var v = referenceValue!;
                     var v = referenceValue!;
                     result = Unsafe.As<object, T>(ref v);
                     result = Unsafe.As<object, T>(ref v);
@@ -283,7 +283,7 @@ public readonly struct LuaValue : IEquatable<LuaValue>
         referenceValue = value;
         referenceValue = value;
     }
     }
 
 
-    public LuaValue(LuaUserData value)
+    public LuaValue(ILuaUserData value)
     {
     {
         type = LuaValueType.UserData;
         type = LuaValueType.UserData;
         referenceValue = value;
         referenceValue = value;
@@ -319,11 +319,6 @@ public readonly struct LuaValue : IEquatable<LuaValue>
         return new(value);
         return new(value);
     }
     }
 
 
-    public static implicit operator LuaValue(LuaUserData value)
-    {
-        return new(value);
-    }
-
     public override int GetHashCode()
     public override int GetHashCode()
     {
     {
         return HashCode.Combine(type, value, referenceValue);
         return HashCode.Combine(type, value, referenceValue);

+ 4 - 3
src/Lua/Standard/FileHandle.cs

@@ -5,7 +5,7 @@ namespace Lua.Standard;
 
 
 // TODO: optimize (remove StreamReader/Writer)
 // TODO: optimize (remove StreamReader/Writer)
 
 
-public class FileHandle : LuaUserData
+public class FileHandle : ILuaUserData
 {
 {
     public static readonly LuaFunction IndexMetamethod = new("index", (context, buffer, ct) =>
     public static readonly LuaFunction IndexMetamethod = new("index", (context, buffer, ct) =>
     {
     {
@@ -41,7 +41,9 @@ public class FileHandle : LuaUserData
 
 
     public bool IsClosed => Volatile.Read(ref isClosed);
     public bool IsClosed => Volatile.Read(ref isClosed);
 
 
-    static readonly LuaTable fileHandleMetatable;
+    LuaTable? ILuaUserData.Metatable { get => fileHandleMetatable; set => fileHandleMetatable = value; }
+
+    static LuaTable? fileHandleMetatable;
 
 
     static FileHandle()
     static FileHandle()
     {
     {
@@ -54,7 +56,6 @@ public class FileHandle : LuaUserData
         this.stream = stream;
         this.stream = stream;
         if (stream.CanRead) reader = new StreamReader(stream);
         if (stream.CanRead) reader = new StreamReader(stream);
         if (stream.CanWrite) writer = new StreamWriter(stream);
         if (stream.CanWrite) writer = new StreamWriter(stream);
-        Metatable = fileHandleMetatable;
     }
     }
 
 
     public string? ReadLine()
     public string? ReadLine()

+ 11 - 11
src/Lua/Standard/IOLibrary.cs

@@ -13,9 +13,9 @@ public static class IOLibrary
             io[func.Name] = func;
             io[func.Name] = func;
         }
         }
 
 
-        io["stdio"] = new FileHandle(Console.OpenStandardInput());
-        io["stdout"] = new FileHandle(Console.OpenStandardOutput());
-        io["stderr"] = new FileHandle(Console.OpenStandardError());
+        io["stdio"] = new LuaValue(new FileHandle(Console.OpenStandardInput()));
+        io["stdout"] = new LuaValue(new FileHandle(Console.OpenStandardOutput()));
+        io["stderr"] = new LuaValue(new FileHandle(Console.OpenStandardError()));
 
 
         state.Environment["io"] = io;
         state.Environment["io"] = io;
         state.LoadedModules["io"] = io;
         state.LoadedModules["io"] = io;
@@ -86,16 +86,16 @@ public static class IOLibrary
         var arg = context.Arguments[0];
         var arg = context.Arguments[0];
         if (arg.TryRead<FileHandle>(out var file))
         if (arg.TryRead<FileHandle>(out var file))
         {
         {
-            io["stdio"] = file;
-            buffer.Span[0] = file;
+            io["stdio"] = new(file);
+            buffer.Span[0] = new(file);
             return new(1);
             return new(1);
         }
         }
         else
         else
         {
         {
             var stream = File.Open(arg.ToString()!, FileMode.Open, FileAccess.ReadWrite);
             var stream = File.Open(arg.ToString()!, FileMode.Open, FileAccess.ReadWrite);
             var handle = new FileHandle(stream);
             var handle = new FileHandle(stream);
-            io["stdio"] = handle;
-            buffer.Span[0] = handle;
+            io["stdio"] = new(handle);
+            buffer.Span[0] = new(handle);
             return new(1);
             return new(1);
         }
         }
     }
     }
@@ -164,16 +164,16 @@ public static class IOLibrary
         var arg = context.Arguments[0];
         var arg = context.Arguments[0];
         if (arg.TryRead<FileHandle>(out var file))
         if (arg.TryRead<FileHandle>(out var file))
         {
         {
-            io["stdout"] = file;
-            buffer.Span[0] = file;
+            io["stdout"] = new(file);
+            buffer.Span[0] = new(file);
             return new(1);
             return new(1);
         }
         }
         else
         else
         {
         {
             var stream = File.Open(arg.ToString()!, FileMode.Open, FileAccess.ReadWrite);
             var stream = File.Open(arg.ToString()!, FileMode.Open, FileAccess.ReadWrite);
             var handle = new FileHandle(stream);
             var handle = new FileHandle(stream);
-            io["stdout"] = handle;
-            buffer.Span[0] = handle;
+            io["stdout"] = new(handle);
+            buffer.Span[0] = new(handle);
             return new(1);
             return new(1);
         }
         }
     }
     }

+ 2 - 2
src/Lua/Standard/Internal/IOHelper.cs

@@ -25,7 +25,7 @@ internal static class IOHelper
         try
         try
         {
         {
             var stream = File.Open(fileName, fileMode, fileAccess);
             var stream = File.Open(fileName, fileMode, fileAccess);
-            buffer.Span[0] = new FileHandle(stream);
+            buffer.Span[0] = new LuaValue(new FileHandle(stream));
             return 1;
             return 1;
         }
         }
         catch (IOException ex)
         catch (IOException ex)
@@ -76,7 +76,7 @@ internal static class IOHelper
             return 3;
             return 3;
         }
         }
 
 
-        buffer.Span[0] = file;
+        buffer.Span[0] = new(file);
         return 1;
         return 1;
     }
     }
 
 

+ 10 - 3
src/Lua/Standard/MathematicsLibrary.cs

@@ -4,7 +4,7 @@ public static class MathematicsLibrary
 {
 {
     public static void OpenMathLibrary(this LuaState state)
     public static void OpenMathLibrary(this LuaState state)
     {
     {
-        state.Environment[RandomInstanceKey] = new LuaUserData<Random>(new Random());
+        state.Environment[RandomInstanceKey] = new(new RandomUserData(new Random()));
 
 
         var math = new LuaTable(0, Functions.Length);
         var math = new LuaTable(0, Functions.Length);
         foreach (var func in Functions)
         foreach (var func in Functions)
@@ -51,6 +51,13 @@ public static class MathematicsLibrary
         new("tanh", Tanh),
         new("tanh", Tanh),
     ];
     ];
 
 
+    sealed class RandomUserData(Random random) : ILuaUserData
+    {
+        static LuaTable? SharedMetatable;
+        public LuaTable? Metatable { get => SharedMetatable; set => SharedMetatable = value; }
+        public Random Random { get; } = random;
+    }
+
     public static ValueTask<int> Abs(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
     public static ValueTask<int> Abs(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
     {
     {
         var arg0 = context.GetArgument<double>(0);
         var arg0 = context.GetArgument<double>(0);
@@ -227,7 +234,7 @@ public static class MathematicsLibrary
 
 
     public static ValueTask<int> Random(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
     public static ValueTask<int> Random(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
     {
     {
-        var rand = context.State.Environment[RandomInstanceKey].Read<LuaUserData<Random>>().Value;
+        var rand = context.State.Environment[RandomInstanceKey].Read<RandomUserData>().Random;
 
 
         if (context.ArgumentCount == 0)
         if (context.ArgumentCount == 0)
         {
         {
@@ -251,7 +258,7 @@ public static class MathematicsLibrary
     public static ValueTask<int> RandomSeed(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
     public static ValueTask<int> RandomSeed(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
     {
     {
         var arg0 = context.GetArgument<double>(0);
         var arg0 = context.GetArgument<double>(0);
-        context.State.Environment[RandomInstanceKey] = new LuaUserData<Random>(new Random((int)BitConverter.DoubleToInt64Bits(arg0)));
+        context.State.Environment[RandomInstanceKey] = new(new RandomUserData(new Random((int)BitConverter.DoubleToInt64Bits(arg0))));
         return new(0);
         return new(0);
     }
     }
 
 

+ 4 - 0
tests/Lua.Tests/Lua.Tests.csproj

@@ -19,6 +19,10 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Lua\Lua.csproj" />
     <ProjectReference Include="..\..\src\Lua\Lua.csproj" />
+    <ProjectReference Include="..\..\src\Lua.SourceGenerator\Lua.SourceGenerator.csproj">
+			<OutputItemType>Analyzer</OutputItemType>
+			<ReferenceOutputAssembly>true</ReferenceOutputAssembly>
+		</ProjectReference>
   </ItemGroup>
   </ItemGroup>
 
 
 </Project>
 </Project>

+ 122 - 0
tests/Lua.Tests/LuaObjectTests.cs

@@ -0,0 +1,122 @@
+namespace Lua.Tests;
+
+[LuaObject]
+public partial class TestUserData
+{
+    [LuaMember]
+    public double Property { get; set; }
+
+    [LuaMember("p2")]
+    public string PropertyWithName { get; set; } = "";
+
+    [LuaMember]
+    public static void MethodVoid()
+    {
+        Console.WriteLine("HEY!");
+    }
+
+    [LuaMember]
+    public static async Task MethodAsync()
+    {
+        await Task.CompletedTask;
+    }
+
+    public static double StaticMethodWithReturnValue(double a, double b)
+    {
+        return a + b;
+    }
+
+    [LuaMember]
+    public double InstanceMethodWithReturnValue()
+    {
+        return Property;
+    }
+}
+
+public class LuaObjectTests
+{
+    [Test]
+    public async Task Test_Property()
+    {
+        var userData = new TestUserData()
+        {
+            Property = 1
+        };
+
+        var state = LuaState.Create();
+        state.Environment["test"] = userData;
+        var results = await state.DoStringAsync("return test.Property");
+
+        Assert.That(results, Has.Length.EqualTo(1));
+        Assert.That(results[0], Is.EqualTo(new LuaValue(1)));
+    }
+
+    [Test]
+    public async Task Test_PropertyWithName()
+    {
+        var userData = new TestUserData()
+        {
+            PropertyWithName = "foo",
+        };
+
+        var state = LuaState.Create();
+        state.Environment["test"] = userData;
+        var results = await state.DoStringAsync("return test.p2");
+
+        Assert.That(results, Has.Length.EqualTo(1));
+        Assert.That(results[0], Is.EqualTo(new LuaValue("foo")));
+    }
+
+    [Test]
+    public async Task Test_MethodVoid()
+    {
+        var userData = new TestUserData();
+
+        var state = LuaState.Create();
+        state.Environment["test"] = userData;
+        var results = await state.DoStringAsync("return test.MethodVoid()");
+
+        Assert.That(results, Has.Length.EqualTo(0));
+    }
+
+    [Test]
+    public async Task Test_MethodAsync()
+    {
+        var userData = new TestUserData();
+
+        var state = LuaState.Create();
+        state.Environment["test"] = userData;
+        var results = await state.DoStringAsync("return test.MethodAsync()");
+
+        Assert.That(results, Has.Length.EqualTo(0));
+    }
+
+    [Test]
+    public async Task Test_StaticMethodWithReturnValue()
+    {
+        var userData = new TestUserData();
+
+        var state = LuaState.Create();
+        state.Environment["test"] = userData;
+        var results = await state.DoStringAsync("return test.StaticMethodWithReturnValue(1, 2)");
+
+        Assert.That(results, Has.Length.EqualTo(1));
+        Assert.That(results[0], Is.EqualTo(new LuaValue(3)));
+    }
+
+    [Test]
+    public async Task Test_InstanceMethodWithReturnValue()
+    {
+        var userData = new TestUserData()
+        {
+            Property = 1
+        };
+
+        var state = LuaState.Create();
+        state.Environment["test"] = userData;
+        var results = await state.DoStringAsync("return test:InstanceMethodWithReturnValue()");
+
+        Assert.That(results, Has.Length.EqualTo(1));
+        Assert.That(results[0], Is.EqualTo(new LuaValue(1)));
+    }
+}