Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,13 @@ return add(1, 2)
> [!TIP]
> Defining functions with `LuaFunction` can be somewhat verbose. When adding multiple functions, it is recommended to use the Source Generator with the `[LuaObject]` attribute. For more details, see the [LuaObject](#luaobject) section.

### Binding a C# Target

`Lua.Runtime.BoundClosure<T>` retains a typed target for custom bindings and generators.
Inside a static callback, cast `context.State.GetCurrentFrame().Function` to
`BoundClosure<T>` and read its `Target`. The target is separate from Lua arguments
and upvalues.

## Low-Level API

In addition to normal function calls, it is possible to directly call Lua's low-level API.
Expand Down Expand Up @@ -453,6 +460,22 @@ Return types such as `void`, `Task/Task<T>`, `ValueTask/ValueTask<T>`, `UniTask/

If the type is not supported, the Source Generator will output a compile-time error.

### Bound instance methods

Set `[LuaObject(BindInstanceMethods = true)]` to bind instance methods to their
object and call them with dot notation. For the `LuaVector3` example above:

```lua
local v2 = v1.normalized()
local normalize = v1.normalized
local v3 = normalize()
```

Bound methods are cached per object and read all supplied arguments as method
parameters. The option defaults to `false`, which uses explicit receivers
(`obj:Method(...)` or `obj.Method(obj, ...)`). Static methods and metamethods keep
their existing calling conventions.

### Variable-length arguments

A final `ReadOnlySpan<LuaValue>` or `ReadOnlyMemory<LuaValue>` parameter receives the remaining Lua arguments. An injected `CancellationToken` may follow it. Fixed arguments are read first; an instance method's `self` is excluded, and `out` parameters consume no Lua arguments. No remaining arguments produces an empty view, and `nil` values are preserved.
Expand Down
9 changes: 9 additions & 0 deletions README_JA.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,15 @@ print(v2.x, v2.y, v2.z)

利用可能でない型に対してはSource Generatorがコンパイルエラーを出力します。

### インスタンスメソッドのバインド

`[LuaObject(BindInstanceMethods = true)]`を指定すると、インスタンスメソッドを
`obj.Method(...)`で呼び出せます。`local method = obj.Method; method(...)`のように
取り出した関数も元のオブジェクトを保持し、オブジェクトごとにキャッシュされます。
このモードでは、渡した引数はすべてメソッドの引数として扱われます。
staticメソッドとメタメソッドの呼び出し規約は変わりません。
既定値は`false`で、従来の`obj:Method(...)`または`obj.Method(obj, ...)`を使用します。

### LuaMetamethod

`[LuaMetamethod]`属性を追加することで、C#のメソッドをLua側で使用されるメタメソッドとして設定することが可能です。
Expand Down
4 changes: 4 additions & 0 deletions src/Lua.Annotations/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public LuaObjectAttribute(string name)
}

public string? Name { get; }

/// <summary>Gets or sets whether instance methods are bound to their owning object.</summary>
/// <remarks>When true, instance methods use dot notation without an explicit receiver.</remarks>
public bool BindInstanceMethods { get; set; }
}

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)]
Expand Down
117 changes: 106 additions & 11 deletions src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -639,12 +639,17 @@ TempCollections tempCollections
);
}

var boundMethodIndex = 0;
foreach (
var methodMetadata in typeMetadata.Methods.Where(x => x.HasMemberAttribute)
)
{
var function =
methodMetadata.IsStatic || !typeMetadata.BindInstanceMethods
? $"__function_{methodMetadata.LuaMemberName}"
: $"userData.__GetBoundMethod({boundMethodIndex++}, \"{methodMetadata.LuaMemberName}\", __function_{methodMetadata.LuaMemberName})";
builder.AppendLine(
@$"if (stringKey == ""{methodMetadata.LuaMemberName}"") return new global::System.Threading.Tasks.ValueTask<int>(context.Return(new global::Lua.LuaValue(__function_{methodMetadata.LuaMemberName})));"
@$"if (stringKey == ""{methodMetadata.LuaMemberName}"") return new global::System.Threading.Tasks.ValueTask<int>(context.Return(new global::Lua.LuaValue({function})));"
);
}

Expand Down Expand Up @@ -976,6 +981,13 @@ in SourceProductionContext context
)
{
builder.AppendLine();
var boundMethodCount = typeMetadata.BindInstanceMethods
? typeMetadata.Methods.Count(x => x.HasMemberAttribute && !x.IsStatic)
: 0;
if (boundMethodCount > 0)
{
EmitBoundMethodCache(typeMetadata, boundMethodCount, builder);
}

foreach (var methodMetadata in typeMetadata.Methods)
{
Expand All @@ -991,7 +1003,8 @@ in SourceProductionContext context
methodMetadata,
builder,
references,
compilation
compilation,
bindTarget: typeMetadata.BindInstanceMethods && !methodMetadata.IsStatic
);
}

Expand All @@ -1012,7 +1025,10 @@ in SourceProductionContext context
continue;
}

if (functionName == null)
if (
functionName == null
|| (typeMetadata.BindInstanceMethods && !methodMetadata.IsStatic)
)
{
if (
methodMetadata.Metamethod
Expand Down Expand Up @@ -1047,26 +1063,104 @@ and not LuaObjectMetamethod.NewIndex
return true;
}

static void EmitBoundMethodCache(
TypeMetadata typeMetadata,
int methodCount,
CodeBuilder builder
)
{
var closureType = $"global::Lua.Runtime.BoundClosure<{typeMetadata.FullTypeName}>";
if (typeMetadata.Symbol.IsRecord)
{
builder.AppendLine(
$"static readonly global::System.Runtime.CompilerServices.ConditionalWeakTable<{typeMetadata.FullTypeName}, {closureType}?[]> __boundMethods = new();"
);
}
else
{
builder.AppendLine($"{closureType}?[]? __boundMethods;");
}

builder.AppendLine(
$"{closureType} __GetBoundMethod(int index, string name, global::System.Func<global::Lua.LuaFunctionExecutionContext, global::System.Threading.CancellationToken, global::System.Threading.Tasks.ValueTask<int>> func)"
);
using (builder.BeginBlockScope())
{
if (typeMetadata.Symbol.IsRecord)
{
builder.AppendLine(
$"var methods = __boundMethods.GetValue(this, static _ => new {closureType}?[{methodCount}]);"
);
}
else
{
builder.AppendLine(
"var methods = global::System.Threading.Volatile.Read(ref __boundMethods);"
);
builder.AppendLine("if (methods == null)");
using (builder.BeginBlockScope())
{
builder.AppendLine($"methods = new {closureType}?[{methodCount}];");
builder.AppendLine(
"methods = global::System.Threading.Interlocked.CompareExchange(ref __boundMethods, methods, null) ?? methods;"
);
}
}
builder.AppendLine(
"var function = global::System.Threading.Volatile.Read(ref methods[index]);"
);
builder.AppendLine("if (function != null) return function;");
builder.AppendLine($"function = new {closureType}(name, this, func);");
builder.AppendLine(
"return global::System.Threading.Interlocked.CompareExchange(ref methods[index], function, null) ?? function;"
);
}
}

static void EmitMethodFunction(
string functionName,
string chunkName,
TypeMetadata typeMetadata,
MethodMetadata methodMetadata,
CodeBuilder builder,
SymbolReferences references,
Compilation compilation
Compilation compilation,
bool bindTarget = false
)
{
builder.AppendLine(
$@"static readonly global::Lua.LuaFunction {functionName} = new global::Lua.LuaFunction(""{chunkName}"", {(methodMetadata.IsAsync ? "async" : "")} (context, ct) =>"
);
if (bindTarget)
{
builder.AppendLine(
$"static readonly global::System.Func<global::Lua.LuaFunctionExecutionContext, global::System.Threading.CancellationToken, global::System.Threading.Tasks.ValueTask<int>> {functionName} = static {(methodMetadata.IsAsync ? "async" : "")} (context, ct) =>"
);
}
else
{
builder.AppendLine(
$@"static readonly global::Lua.LuaFunction {functionName} = new global::Lua.LuaFunction(""{chunkName}"", {(methodMetadata.IsAsync ? "async" : "")} (context, ct) =>"
);
}

using (builder.BeginBlockScope())
{
EmitMethodFunctionBody(typeMetadata, methodMetadata, builder, references, compilation);
if (bindTarget)
{
builder.AppendLine(
$"var userData = ((global::Lua.Runtime.BoundClosure<{typeMetadata.FullTypeName}>)context.State.GetCurrentFrame().Function).Target;"
);
}
EmitMethodFunctionBody(
typeMetadata,
methodMetadata,
builder,
references,
compilation,
existingUserDataVariable: bindTarget ? "userData" : null,
boundTarget: bindTarget
);
}

builder.AppendLine(");");
builder.AppendLine(bindTarget ? ";" : ");");
builder.AppendLine();
}

Expand All @@ -1077,7 +1171,8 @@ static void EmitMethodFunctionBody(
SymbolReferences references,
Compilation compilation,
IReadOnlyDictionary<int, string>? preloadedArguments = null,
string? existingUserDataVariable = null
string? existingUserDataVariable = null,
bool boundTarget = false
)
{
var index = 0;
Expand All @@ -1086,7 +1181,7 @@ static void EmitMethodFunctionBody(
var extraReturnValues = new List<string>();
var userDataVariable = existingUserDataVariable ?? "userData";

if (!methodMetadata.IsStatic)
if (!methodMetadata.IsStatic && !boundTarget)
{
if (existingUserDataVariable == null)
{
Expand Down
6 changes: 6 additions & 0 deletions src/Lua.SourceGenerator/TypeMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class TypeMetadata
public INamedTypeSymbol Symbol { get; }
public string TypeName { get; }
public string FullTypeName { get; }
public bool BindInstanceMethods { get; }
public PropertyMetadata[] Properties { get; }
public MethodMetadata[] Methods { get; }

Expand All @@ -24,6 +25,11 @@ SymbolReferences references

TypeName = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
FullTypeName = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
BindInstanceMethods =
symbol
.GetAttribute(references.LuaObjectAttribute)
?.NamedArguments.Any(x => x.Key == "BindInstanceMethods" && x.Value.Value is true)
== true;

var properties = new List<PropertyMetadata>();
var methods = new List<MethodMetadata>();
Expand Down
21 changes: 21 additions & 0 deletions src/Lua/Runtime/BoundClosure.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Lua.Runtime;

/// <summary>A managed Lua function with a bound target.</summary>
/// <typeparam name="T">The type of the bound target.</typeparam>
/// <param name="name">The function name used in diagnostics.</param>
/// <param name="target">The target retained by this closure.</param>
/// <param name="func">The managed callback, following the result contract of <see cref="LuaFunction"/>.</param>
/// <remarks>
/// Retrieve this closure through <c>context.State.GetCurrentFrame().Function</c>
/// inside the callback to access its target. The target is separate from Lua
/// arguments and upvalues; this type does not insert or remove a self argument.
/// </remarks>
public sealed class BoundClosure<T>(
string name,
T target,
Func<LuaFunctionExecutionContext, CancellationToken, ValueTask<int>> func
) : LuaFunction(name, func)
{
/// <summary>Gets the target retained by this closure.</summary>
public T Target { get; } = target;
}
27 changes: 27 additions & 0 deletions tests/Lua.SourceGenerator.Tests/LuaObjectGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ namespace Lua.SourceGenerator.Tests;

public class LuaObjectGeneratorTests
{
[TestCase("class BoundObject")]
[TestCase("class BoundObject<T>")]
[TestCase("record BoundObject")]
[TestCase("record BoundObject<T>")]
public void BoundMethods_GenerateWithoutDiagnostics(string declaration)
{
var result = CompileGeneratedSource(
$$"""
using Lua;
[LuaObject(BindInstanceMethods = true)]
internal partial {{declaration}}
{
[LuaMember]
public int Read(int value = 1) => value;

[LuaMember, LuaMetamethod(LuaObjectMetamethod.Len)]
public int Length() => 2;

[LuaMember]
public static int StaticRead() => 3;
}
"""
);
Assert.That(result.Success, Is.True, string.Join("\n", result.Diagnostics));
Assert.That(result.Diagnostics, Is.Empty);
}

[TestCase("public static int Read(System.ReadOnlySpan<LuaValue> values) => values.Length;")]
[TestCase("public int Read(System.ReadOnlySpan<LuaValue> values) => values.Length;")]
[TestCase(
Expand Down
Loading
Loading