Skip to content

Commit 35ea50b

Browse files
committed
Initial async support
1 parent 0b9b200 commit 35ea50b

34 files changed

Lines changed: 3352 additions & 91 deletions
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
using System;
2+
using System.Diagnostics;
3+
using Laylua.Marshaling;
4+
using Laylua.Moon;
5+
6+
namespace Laylua;
7+
8+
internal static unsafe class LuaCoroutineRuntime
9+
{
10+
public static int PushFunctionAndArguments(LuaThread coroutineThread, LuaFunction function, object?[] arguments)
11+
{
12+
PrepareFunctionCall(coroutineThread, function, arguments.Length);
13+
14+
foreach (var argument in arguments)
15+
{
16+
coroutineThread.Stack.Push(argument);
17+
}
18+
19+
return arguments.Length;
20+
}
21+
22+
public static int PushFunctionAndStackArguments(LuaThread coroutineThread, LuaFunction function, ReadOnlySpan<LuaStackValue> arguments)
23+
{
24+
PrepareFunctionCall(coroutineThread, function, arguments.Length);
25+
26+
foreach (var argument in arguments)
27+
{
28+
PushStackArgument(coroutineThread, argument);
29+
}
30+
31+
return arguments.Length;
32+
}
33+
34+
public static int PushFunctionAndStackArguments(LuaThread coroutineThread, LuaFunction function, LuaStackValueRange arguments)
35+
{
36+
PrepareFunctionCall(coroutineThread, function, arguments.Count);
37+
38+
foreach (var argument in arguments)
39+
{
40+
PushStackArgument(coroutineThread, argument);
41+
}
42+
43+
return arguments.Count;
44+
}
45+
46+
private static void PrepareFunctionCall(LuaThread coroutineThread, LuaFunction function, int argumentCount)
47+
{
48+
coroutineThread.Stack.EnsureFreeCapacity(1 + argumentCount);
49+
coroutineThread.Stack.Push(function);
50+
}
51+
52+
private static void PushStackArgument(LuaThread coroutineThread, LuaStackValue argument)
53+
{
54+
var sourceThread = argument.Thread;
55+
if (argument.Type == LuaType.None)
56+
{
57+
throw new InvalidOperationException("The given stack value is not valid.");
58+
}
59+
60+
if (sourceThread.MainThread.State.L != coroutineThread.MainThread.State.L)
61+
{
62+
throw new InvalidOperationException("The given stack value is owned by a different Lua state.");
63+
}
64+
65+
sourceThread.Stack.EnsureFreeCapacity(1);
66+
coroutineThread.Stack.EnsureFreeCapacity(1);
67+
68+
lua_pushvalue(sourceThread.State.L, argument.Index);
69+
lua_xmove(sourceThread.State.L, coroutineThread.State.L, 1);
70+
}
71+
72+
public static ResumeStepResult ResumeStep(LuaThread coroutineThread, LuaThread fromThread, int argumentCount)
73+
{
74+
var coroutineL = coroutineThread.State.L;
75+
var fromL = fromThread.State.L;
76+
var laylua = LayluaState.FromExtraSpace(coroutineL);
77+
var initialPanic = laylua.Panic;
78+
79+
var status = lua_resume(coroutineL, fromL, argumentCount, out var resultCount);
80+
81+
if (status == LuaStatus.Yield)
82+
{
83+
// Inside the yielding C function, lua_yield long-jumps directly back into
84+
// lua_resume, bypassing the assembly trampoline that wraps every native Lua
85+
// export and would normally pop the panic frame it pushed around lua_yieldk.
86+
// Drain that orphaned frame here so the panic stack stays balanced.
87+
while (!ReferenceEquals(laylua.Panic, initialPanic))
88+
{
89+
var leakedPanic = laylua.PopPanic();
90+
if (leakedPanic == null)
91+
{
92+
break;
93+
}
94+
95+
laylua.ResetPanic(leakedPanic);
96+
}
97+
98+
Debug.Assert(ReferenceEquals(laylua.Panic, initialPanic));
99+
100+
if (resultCount != 1 || !AwaitableUserData.IsAwaitable(coroutineL, -1))
101+
{
102+
lua_settop(coroutineL, 0);
103+
throw new LuaException("The Lua coroutine yielded a value that is not an awaitable.");
104+
}
105+
106+
if (!AwaitableUserData.TryGetBox(coroutineL, -1, out var box) || box is null)
107+
{
108+
lua_settop(coroutineL, 0);
109+
throw new LuaException("Failed to extract the awaitable from the yielded user data.");
110+
}
111+
112+
lua_pop(coroutineL);
113+
return new ResumeStepResult(LuaStatus.Yield, box, resultCount: 0);
114+
}
115+
116+
if (status == LuaStatus.Ok)
117+
{
118+
Debug.Assert(ReferenceEquals(laylua.Panic, initialPanic));
119+
return new ResumeStepResult(LuaStatus.Ok, awaitableBox: null, resultCount);
120+
}
121+
122+
Debug.Assert(ReferenceEquals(laylua.Panic, initialPanic));
123+
LuaThread.ThrowLuaException(coroutineThread, status);
124+
return default;
125+
}
126+
127+
public static void ClearStack(LuaThread coroutineThread)
128+
{
129+
lua_settop(coroutineThread.State.L, 0);
130+
}
131+
132+
public static T? ReadFirstResult<T>(LuaThread coroutineThread, int resultCount)
133+
{
134+
try
135+
{
136+
if (resultCount == 0)
137+
{
138+
throw new InvalidOperationException("The code evaluation succeeded, but returned no results.");
139+
}
140+
141+
return coroutineThread.MainThread.Marshaler.GetValue<T>(coroutineThread, 1);
142+
}
143+
finally
144+
{
145+
ClearStack(coroutineThread);
146+
}
147+
}
148+
149+
public static LuaFunctionResults CollectResults(LuaThread coroutineThread, LuaThread destinationThread, int resultCount)
150+
{
151+
if (resultCount < 0)
152+
{
153+
throw new ArgumentOutOfRangeException(nameof(resultCount), "The result count cannot be negative.");
154+
}
155+
156+
var oldTop = destinationThread.Stack.Count;
157+
coroutineThread.Stack.XMove(destinationThread, resultCount);
158+
159+
var range = LuaStackValueRange.FromTop(destinationThread.Stack, oldTop, destinationThread.Stack.Count);
160+
return new LuaFunctionResults(range);
161+
}
162+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using Laylua.Marshaling;
3+
using Laylua.Moon;
4+
5+
namespace Laylua;
6+
7+
internal readonly struct ResumeStepResult
8+
{
9+
public LuaStatus Status { get; }
10+
11+
public AwaitableBox? AwaitableBox { get; }
12+
13+
public int ResultCount { get; }
14+
15+
public ResumeStepResult(LuaStatus status, AwaitableBox? awaitableBox, int resultCount)
16+
{
17+
if (resultCount < 0)
18+
{
19+
throw new ArgumentOutOfRangeException(nameof(resultCount), "The result count cannot be negative.");
20+
}
21+
22+
if (status == LuaStatus.Yield && awaitableBox == null)
23+
{
24+
throw new ArgumentNullException(nameof(awaitableBox), "A yielded coroutine step must provide an awaitable.");
25+
}
26+
27+
if (status != LuaStatus.Yield && awaitableBox != null)
28+
{
29+
throw new ArgumentException("Only a yielded coroutine step can provide an awaitable.", nameof(awaitableBox));
30+
}
31+
32+
Status = status;
33+
AwaitableBox = awaitableBox;
34+
ResultCount = resultCount;
35+
}
36+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace Laylua;
6+
7+
public sealed unsafe partial class LuaFunction
8+
{
9+
/// <summary>
10+
/// Invokes this Lua function with support for awaitables (e.g., <see cref="Task"/>).
11+
/// </summary>
12+
/// <param name="options"> Configures async behavior for this invocation. </param>
13+
/// <param name="cancellationToken"> The cancellation token to observe. </param>
14+
/// <returns>
15+
/// The Lua results produced by the function call.
16+
/// </returns>
17+
/// <remarks>
18+
/// This API does not make Lua execution concurrent. The owning Lua state still executes on one thread at a time.
19+
/// </remarks>
20+
public ValueTask<LuaFunctionResults> CallAsync(LuaAsyncOptions options = default, CancellationToken cancellationToken = default)
21+
{
22+
return CallAsync(Array.Empty<object?>(), options, cancellationToken);
23+
}
24+
25+
/// <inheritdoc cref="CallAsync(LuaAsyncOptions,CancellationToken)"/>
26+
/// <param name="arguments"> The arguments to pass to the function. </param>
27+
/// <param name="cancellationToken"> The cancellation token to observe. </param>
28+
public ValueTask<LuaFunctionResults> CallAsync(object?[] arguments, LuaAsyncOptions options = default, CancellationToken cancellationToken = default)
29+
{
30+
ThrowIfInvalid();
31+
32+
return new ValueTask<LuaFunctionResults>(LuaThread.RunFunctionAsyncCore(this, arguments, options, cancellationToken));
33+
}
34+
35+
/// <inheritdoc cref="CallAsync(object?[],LuaAsyncOptions,CancellationToken)"/>
36+
public ValueTask<LuaFunctionResults> CallAsync<T>(T? argument, LuaAsyncOptions options = default, CancellationToken cancellationToken = default)
37+
{
38+
return CallAsync(
39+
[
40+
argument
41+
],
42+
options,
43+
cancellationToken);
44+
}
45+
46+
/// <inheritdoc cref="CallAsync(object?[],LuaAsyncOptions,CancellationToken)"/>
47+
public ValueTask<LuaFunctionResults> CallAsync<T1, T2>(T1? argument1, T2? argument2, LuaAsyncOptions options = default, CancellationToken cancellationToken = default)
48+
{
49+
return CallAsync(
50+
[
51+
argument1,
52+
argument2
53+
],
54+
options,
55+
cancellationToken);
56+
}
57+
58+
/// <inheritdoc cref="CallAsync(object?[],LuaAsyncOptions,CancellationToken)"/>
59+
public ValueTask<LuaFunctionResults> CallAsync<T1, T2, T3>(T1? argument1, T2? argument2, T3? argument3, LuaAsyncOptions options = default, CancellationToken cancellationToken = default)
60+
{
61+
return CallAsync(
62+
[
63+
argument1,
64+
argument2,
65+
argument3
66+
],
67+
options,
68+
cancellationToken);
69+
}
70+
71+
/// <inheritdoc cref="CallAsync(object?[],LuaAsyncOptions,CancellationToken)"/>
72+
public ValueTask<LuaFunctionResults> CallAsync<T1, T2, T3, T4>(T1? argument1, T2? argument2, T3? argument3, T4? argument4, LuaAsyncOptions options = default, CancellationToken cancellationToken = default)
73+
{
74+
return CallAsync(
75+
[
76+
argument1,
77+
argument2,
78+
argument3,
79+
argument4
80+
],
81+
options,
82+
cancellationToken);
83+
}
84+
85+
/// <inheritdoc cref="CallAsync(object?[],LuaAsyncOptions,CancellationToken)"/>
86+
public ValueTask<LuaFunctionResults> CallAsync(LuaStackValue[] arguments, LuaAsyncOptions options = default, CancellationToken cancellationToken = default)
87+
{
88+
return CallAsync(arguments.AsSpan(), options, cancellationToken);
89+
}
90+
91+
/// <inheritdoc cref="CallAsync(object?[],LuaAsyncOptions,CancellationToken)"/>
92+
public ValueTask<LuaFunctionResults> CallAsync(ReadOnlySpan<LuaStackValue> arguments, LuaAsyncOptions options = default, CancellationToken cancellationToken = default)
93+
{
94+
ThrowIfInvalid();
95+
96+
var lua = Lua;
97+
LuaAsyncLibrary.EnsureInitialized(lua);
98+
99+
var coroutineThread = lua.CreateThread();
100+
try
101+
{
102+
var argumentCount = LuaCoroutineRuntime.PushFunctionAndStackArguments(coroutineThread, this, arguments);
103+
return new ValueTask<LuaFunctionResults>(LuaThread.RunPreparedFunctionAsyncCore(coroutineThread, lua, argumentCount, options, cancellationToken));
104+
}
105+
catch
106+
{
107+
coroutineThread.Dispose();
108+
throw;
109+
}
110+
}
111+
112+
/// <inheritdoc cref="CallAsync(object?[],LuaAsyncOptions,CancellationToken)"/>
113+
public ValueTask<LuaFunctionResults> CallAsync(LuaStackValueRange arguments, LuaAsyncOptions options = default, CancellationToken cancellationToken = default)
114+
{
115+
ThrowIfInvalid();
116+
117+
var lua = Lua;
118+
LuaAsyncLibrary.EnsureInitialized(lua);
119+
120+
var coroutineThread = lua.CreateThread();
121+
try
122+
{
123+
var argumentCount = LuaCoroutineRuntime.PushFunctionAndStackArguments(coroutineThread, this, arguments);
124+
return new ValueTask<LuaFunctionResults>(LuaThread.RunPreparedFunctionAsyncCore(coroutineThread, lua, argumentCount, options, cancellationToken));
125+
}
126+
catch
127+
{
128+
coroutineThread.Dispose();
129+
throw;
130+
}
131+
}
132+
}

src/Laylua/Library/Entities/Reference/LuaReference.cs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Diagnostics;
33
using System.Diagnostics.CodeAnalysis;
44
using System.Runtime.CompilerServices;
5+
using System.Threading;
56
using Laylua.Moon;
67
using Qommon;
78

@@ -55,16 +56,11 @@ private protected LuaReference()
5556

5657
~LuaReference()
5758
{
58-
if (!IsAlive(this, out var lua))
59+
if (!TryPushLeakedReference(out var lua))
5960
{
6061
return;
6162
}
6263

63-
if (!LuaRegistry.IsPersistentReference(_reference))
64-
{
65-
lua.PushLeakedReference(_reference);
66-
}
67-
6864
if (lua.Marshaler.ReturnReference(this))
6965
{
7066
GC.ReRegisterForFinalize(this);
@@ -273,4 +269,26 @@ internal static bool IsAlive(LuaReference reference, [MaybeNullWhen(false)] out
273269
lua = reference.LuaCore;
274270
return lua != null && !lua.IsDisposed && !reference.IsDisposed;
275271
}
272+
273+
internal bool TryPushLeakedReference([MaybeNullWhen(false)] out Lua lua)
274+
{
275+
lua = LuaCore;
276+
if (lua == null || lua.IsDisposed)
277+
{
278+
return false;
279+
}
280+
281+
var reference = Interlocked.Exchange(ref _reference, LUA_NOREF);
282+
if (reference is LUA_NOREF or LUA_REFNIL)
283+
{
284+
return false;
285+
}
286+
287+
if (!LuaRegistry.IsPersistentReference(reference))
288+
{
289+
lua.PushLeakedReference(reference);
290+
}
291+
292+
return true;
293+
}
276294
}

0 commit comments

Comments
 (0)