Skip to content

Commit beca492

Browse files
committed
Implement table to typed userdata conversion support
1 parent ae4e2e8 commit beca492

16 files changed

Lines changed: 1248 additions & 104 deletions

src/Laylua/Library/Marshaler/UserData/Attributes/Enums/LuaAllowedValueConversions.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,14 @@ public enum LuaAllowedValueConversions
8585
AllowOverflow = 1 << 9,
8686

8787
/// <summary>
88-
/// The default policy: all conversions except <see cref="TableToObject"/>.
88+
/// The default policy: all conversions enabled.
8989
/// </summary>
90-
Default = All & ~TableToObject,
90+
Default = All,
9191

9292
/// <summary>
9393
/// All conversions enabled.
9494
/// </summary>
95+
#pragma warning disable CA1069
9596
All = StringToPrimitive | TableToObject | FunctionToCallableTarget | ObjectFallback | PrimitiveToString | StringToChar | StringToEnum | NumberToEnum | TruncateFraction | AllowOverflow,
97+
#pragma warning restore CA1069
9698
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Laylua.Marshaling;
2+
3+
/// <summary>
4+
/// Controls whether source-generated typed userdata can be materialized from Lua tables.
5+
/// </summary>
6+
public enum LuaFromTableConversion
7+
{
8+
/// <summary>
9+
/// Do not emit generated table-to-object conversion support for the type.
10+
/// </summary>
11+
Disabled = 1,
12+
13+
/// <summary>
14+
/// Emit generated table-to-object conversion support when the type has a supported parameterless-construction path.
15+
/// </summary>
16+
WhenTypeHasParameterlessConstructor = 2,
17+
18+
/// <summary>
19+
/// Require generated table-to-object conversion support and report an error when it cannot be emitted.
20+
/// </summary>
21+
Enabled = 3,
22+
}

src/Laylua/Library/Marshaler/UserData/Attributes/LuaTypeAttribute.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,17 @@ public sealed class LuaTypeAttribute : Attribute
149149
/// </remarks>
150150
public LuaAwaitableReturnHandling AwaitableReturnHandling { get; set; }
151151

152+
/// <summary>
153+
/// Gets or sets whether source-generated table-to-object conversion support is emitted for this type.
154+
/// </summary>
155+
/// <remarks>
156+
/// When not set, inherits from <see cref="LuaTypeDefaultsAttribute.FromTableConversion"/>
157+
/// (built-in default: <see cref="LuaFromTableConversion.WhenTypeHasParameterlessConstructor"/>).
158+
/// This controls whether the generated handler supports reading this type from a Lua table;
159+
/// <see cref="BindingConversions"/> still controls whether generated member binding is allowed to attempt table-to-object conversion.
160+
/// </remarks>
161+
public LuaFromTableConversion FromTableConversion { get; set; }
162+
152163
/// <summary>
153164
/// Gets or sets which value conversions are permitted during argument binding
154165
/// and overload resolution for this type's generated callbacks.

src/Laylua/Library/Marshaler/UserData/Attributes/LuaTypeDefaultsAttribute.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,17 @@ public sealed class LuaTypeDefaultsAttribute : Attribute
152152
/// </remarks>
153153
public LuaAwaitableReturnHandling AwaitableReturnHandling { get; set; } = LuaAwaitableReturnHandling.ImplicitAwait;
154154

155+
/// <summary>
156+
/// Gets or sets the default source-generated table-to-object conversion support mode.
157+
/// </summary>
158+
/// <remarks>
159+
/// Defaults to <see cref="LuaFromTableConversion.WhenTypeHasParameterlessConstructor"/>.
160+
/// Applied when <see cref="LuaTypeAttribute.FromTableConversion"/> is not set.
161+
/// This controls whether generated handlers support reading their own type from a Lua table;
162+
/// <see cref="BindingConversions"/> still controls whether generated member binding is allowed to attempt table-to-object conversion.
163+
/// </remarks>
164+
public LuaFromTableConversion FromTableConversion { get; set; } = LuaFromTableConversion.WhenTypeHasParameterlessConstructor;
165+
155166
/// <summary>
156167
/// Gets or sets the default binding conversion policy for generated callbacks.
157168
/// </summary>

src/Laylua/Library/Marshaler/UserData/DynamicUserDataHandler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,8 +1157,8 @@ public bool TryGetFromTable(lua_State* L, int stackIndex, out object? value)
11571157
{
11581158
if (lua_type(L, -1) != LuaType.Nil)
11591159
{
1160-
var propType = _writableProperties[luaName].PropertyType;
1161-
if (!TryReadWithMarshaler(thread, -1, propType, out var fieldValue))
1160+
var propertyType = _writableProperties[luaName].PropertyType;
1161+
if (!TryReadWithMarshaler(thread, -1, propertyType, out var fieldValue))
11621162
{
11631163
value = default;
11641164
return false;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace Laylua.Marshaling;
4+
5+
/// <summary>
6+
/// Defines a static conversion contract for reading a CLR value from a Lua table.
7+
/// </summary>
8+
/// <typeparam name="TUserData"> The CLR type being materialized. </typeparam>
9+
public interface ILuaUserDataFromTable<TUserData>
10+
{
11+
/// <summary>
12+
/// Attempts to materialize a value from the specified Lua table.
13+
/// </summary>
14+
/// <param name="thread"> The owning Lua thread. </param>
15+
/// <param name="table"> The source Lua table. </param>
16+
/// <param name="value"> The materialized value when conversion succeeds. </param>
17+
/// <returns> <see langword="true"/> when conversion succeeded; otherwise <see langword="false"/>. </returns>
18+
static abstract bool TryGetFromTable(LuaThread thread, LuaTable table, [MaybeNullWhen(false)] out TUserData value);
19+
}

tests/Laylua.Tests/Tests/Analyzers/LuaTypeAnalyzerTests.cs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public sealed class LuaTypeAnalyzerTests
1919
private const string LuaTypeInvalidEnumConfigDiagnosticId = "LAYLUA107";
2020
private const string LuaTypeInvalidFlagsConfigDiagnosticId = "LAYLUA108";
2121
private const string LuaTypeNameCollisionDiagnosticId = "LAYLUA109";
22+
private const string LuaTypeUnsupportedFromTableConversionDiagnosticId = "LAYLUA110";
2223

2324
#if NET8_0
2425
private const string TargetFramework = "net8.0";
@@ -391,6 +392,128 @@ public static int create()
391392
Assert.That(result.Output, Does.Contain(LuaTypeNameCollisionDiagnosticId));
392393
}
393394

395+
[Test]
396+
public void FromTableConversion_EnabledWithoutParameterlessConstructor_ReportsGeneratorError()
397+
{
398+
// Arrange
399+
const string source = """
400+
using Laylua;
401+
using Laylua.Marshaling;
402+
403+
[LuaType(FromTableConversion = LuaFromTableConversion.Enabled)]
404+
public partial class Sample
405+
{
406+
public Sample(string value)
407+
{
408+
}
409+
}
410+
""";
411+
412+
// Act
413+
var result = BuildSource(source);
414+
415+
// Assert
416+
Assert.That(result.ExitCode, Is.Not.EqualTo(0));
417+
Assert.That(result.Output, Does.Contain(LuaTypeUnsupportedFromTableConversionDiagnosticId));
418+
}
419+
420+
[Test]
421+
public void FromTableConversion_OptionalInitOnlyMember_ReportsGeneratorError()
422+
{
423+
// Arrange
424+
const string source = """
425+
using Laylua.Marshaling;
426+
427+
[LuaType]
428+
public partial class Sample
429+
{
430+
public string Name { get; init; } = "";
431+
}
432+
""";
433+
434+
// Act
435+
var result = BuildSource(source);
436+
437+
// Assert
438+
Assert.That(result.ExitCode, Is.Not.EqualTo(0));
439+
Assert.That(result.Output, Does.Contain(LuaTypeUnsupportedFromTableConversionDiagnosticId));
440+
}
441+
442+
[Test]
443+
public void FromTableConversion_IgnoredRequiredMember_ReportsGeneratorError()
444+
{
445+
// Arrange
446+
const string source = """
447+
using Laylua.Marshaling;
448+
449+
[LuaType]
450+
public partial class Sample
451+
{
452+
[LuaIgnore]
453+
public required string Hidden { get; init; }
454+
455+
public int Visible { get; set; }
456+
}
457+
""";
458+
459+
// Act
460+
var result = BuildSource(source);
461+
462+
// Assert
463+
Assert.That(result.ExitCode, Is.Not.EqualTo(0));
464+
Assert.That(result.Output, Does.Contain(LuaTypeUnsupportedFromTableConversionDiagnosticId));
465+
}
466+
467+
[Test]
468+
public void TableConversionMethod_NonPublicCustomSignature_DoesNotReportGeneratorError()
469+
{
470+
// Arrange
471+
const string source = """
472+
using Laylua;
473+
using Laylua.Marshaling;
474+
475+
[LuaType]
476+
public partial class Sample
477+
{
478+
private static bool TryGetFromTable(LuaThread thread, LuaTable table, out Sample value)
479+
{
480+
value = new Sample();
481+
return true;
482+
}
483+
}
484+
""";
485+
486+
// Act
487+
var result = BuildSource(source);
488+
489+
// Assert
490+
Assert.That(result.ExitCode, Is.EqualTo(0), result.Output);
491+
Assert.That(result.Output, Does.Not.Contain(LuaTypeUnsupportedFromTableConversionDiagnosticId));
492+
}
493+
494+
[Test]
495+
public void TypeMember_NamedBindingConversions_DoesNotCollideWithGeneratedTableConversion()
496+
{
497+
// Arrange
498+
const string source = """
499+
using Laylua.Marshaling;
500+
501+
[LuaType]
502+
public partial class Sample
503+
{
504+
private const int BindingConversions = 42;
505+
506+
public required string Name { get; init; }
507+
}
508+
""";
509+
510+
// Act
511+
var result = BuildSource(source);
512+
513+
// Assert
514+
Assert.That(result.ExitCode, Is.EqualTo(0), result.Output);
515+
}
516+
394517
private static BuildResult BuildSource(string source)
395518
{
396519
var tempPath = Path.Combine(Path.GetTempPath(), "LayluaAnalyzerTests", Guid.NewGuid().ToString("N"));

tests/Laylua.Tests/Tests/Marshaler/UserData/Dynamic/DynamicImplicitConversionTests.cs

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ public string AcceptsTarget(ImplicitConversionTarget target)
7474
return $"target:{target.Name}:{target.Value}";
7575
}
7676

77+
public string AcceptsConstructorTarget(DynamicTableConstructorTarget target)
78+
{
79+
return $"target:{target.Name}:{target.Health}:{target.ConstructorUsed}";
80+
}
81+
7782
public string NumericOrObject(double number)
7883
{
7984
return $"double:{number}";
@@ -96,7 +101,7 @@ public string LuaStringOnly(LuaString text)
96101

97102
public string PointerOnly(IntPtr pointer)
98103
{
99-
return $"ptr:0x{pointer.ToString("x")}";
104+
return $"ptr:0x{pointer:x}";
100105
}
101106

102107
public string CharOnly(char value)
@@ -135,13 +140,35 @@ public string StringOrEnum(ImplicitConversionStatus value)
135140
}
136141
}
137142

138-
public class ImplicitConversionConstructable
143+
public class ImplicitConversionConstructable(ImplicitConversionTarget target)
144+
{
145+
public string Summary { get; } = $"target:{target.Name}:{target.Value}";
146+
}
147+
148+
public class DynamicTableRequiredInitTarget
149+
{
150+
public required string Name { get; init; }
151+
152+
public int Health { get; set; }
153+
}
154+
155+
public class DynamicTableConstructorTarget
139156
{
140-
public string Summary { get; }
157+
public string Name { get; }
158+
159+
public int Health { get; set; }
141160

142-
public ImplicitConversionConstructable(ImplicitConversionTarget target)
161+
public bool ConstructorUsed { get; }
162+
163+
public DynamicTableConstructorTarget()
143164
{
144-
Summary = $"target:{target.Name}:{target.Value}";
165+
Name = "parameterless";
166+
}
167+
168+
public DynamicTableConstructorTarget(string name)
169+
{
170+
Name = name;
171+
ConstructorUsed = true;
145172
}
146173
}
147174

@@ -154,18 +181,22 @@ public override void SetUp()
154181
LuaTypeRegistry.Unregister<ImplicitConversionTarget>();
155182
LuaTypeRegistry.Unregister<ImplicitConversionOverloaded>();
156183
LuaTypeRegistry.Unregister<ImplicitConversionConstructable>();
184+
LuaTypeRegistry.Unregister<DynamicTableRequiredInitTarget>();
185+
LuaTypeRegistry.Unregister<DynamicTableConstructorTarget>();
157186
}
158187

159188
public override void TearDown()
160189
{
161190
LuaTypeRegistry.Unregister<ImplicitConversionTarget>();
162191
LuaTypeRegistry.Unregister<ImplicitConversionOverloaded>();
163192
LuaTypeRegistry.Unregister<ImplicitConversionConstructable>();
193+
LuaTypeRegistry.Unregister<DynamicTableRequiredInitTarget>();
194+
LuaTypeRegistry.Unregister<DynamicTableConstructorTarget>();
164195
base.TearDown();
165196
}
166197

167198
[Test]
168-
public void DefaultPolicy_Table_DoesNotBindToTypedUserDataParameter()
199+
public void DefaultPolicy_Table_RemainsBehindObjectFallbackDuringOverloadResolution()
169200
{
170201
// Arrange
171202
LuaTypeRegistry.ConfigureDynamic<ImplicitConversionTarget>(static options =>
@@ -189,7 +220,7 @@ public void DefaultPolicy_Table_DoesNotBindToTypedUserDataParameter()
189220
}
190221

191222
[Test]
192-
public void DefaultPolicy_Table_DoesNotBindToTypedMethodParameter()
223+
public void DefaultPolicy_Table_BindsToTypedMethodParameter()
193224
{
194225
// Arrange
195226
LuaTypeRegistry.ConfigureDynamic<ImplicitConversionTarget>(static options =>
@@ -205,8 +236,11 @@ public void DefaultPolicy_Table_DoesNotBindToTypedMethodParameter()
205236
var obj = new ImplicitConversionOverloaded();
206237
Lua["obj"] = obj;
207238

208-
// Act & Assert
209-
Assert.Throws<LuaException>(() => Lua.Execute("obj:AcceptsTarget({Name = 'test'})"));
239+
// Act
240+
var result = Lua.Evaluate<string>("return obj:AcceptsTarget({Name = 'test'})");
241+
242+
// Assert
243+
Assert.That(result, Is.EqualTo("target:test:0"));
210244
}
211245

212246
[Test]
@@ -436,7 +470,7 @@ public void ObjectFallback_LowerPriority_ThanSpecificOverloads()
436470
}
437471

438472
[Test]
439-
public void DefaultPolicy_Table_DoesNotBindToTypedConstructorParameter()
473+
public void DefaultPolicy_Table_BindsToTypedConstructorParameter()
440474
{
441475
// Arrange
442476
LuaTypeRegistry.ConfigureDynamic<ImplicitConversionTarget>(static options =>
@@ -452,8 +486,12 @@ public void DefaultPolicy_Table_DoesNotBindToTypedConstructorParameter()
452486

453487
Lua["ImplicitConversionConstructable"] = typeof(ImplicitConversionConstructable);
454488

455-
// Act & Assert
456-
Assert.Throws<LuaException>(() => Lua.Execute("return ImplicitConversionConstructable({Name = 'test', Value = 42})"));
489+
// Act
490+
var result = Lua.Evaluate<ImplicitConversionConstructable>("return ImplicitConversionConstructable({Name = 'test', Value = 42})");
491+
492+
// Assert
493+
Assert.That(result, Is.Not.Null);
494+
Assert.That(result!.Summary, Is.EqualTo("target:test:42"));
457495
}
458496

459497
[Test]
@@ -597,7 +635,7 @@ public void LightUserData_DirectMatch_BindsPointerParameter()
597635

598636
var obj = new ImplicitConversionOverloaded();
599637
Lua["obj"] = obj;
600-
SetGlobalLightUserData("pointer", (IntPtr) 0x1234);
638+
SetGlobalLightUserData("pointer", 0x1234);
601639

602640
// Act
603641
var result = Lua.Evaluate<string>("return obj:PointerOnly(pointer)");

0 commit comments

Comments
 (0)