Skip to content

Commit f6813c3

Browse files
authored
Merge pull request #395 from nuskey8/codex/fix-standard-library-contracts
Fix Lua 5.2 library contracts and bytecode interoperability
2 parents 3fbaaea + 0f650c3 commit f6813c3

52 files changed

Lines changed: 4175 additions & 935 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,10 @@ Paths and names passed to `ILuaFileSystem`, `ILuaFileLoader` and `ILuaModuleLoad
773773

774774
Files, temporary files and redirected standard I/O preserve bytes, including a BOM read as data. Raw line reads stop at LF and retain CR; `*L` also retains the LF. Invalid UTF-8 is replaced when decoding with `DecodeUtf8`, so keep binary data in byte strings or byte arrays. Passing unconverted .NET characters above U+00FF to raw output writes `?` for those characters.
775775

776+
Lua's `load` accepts source strings and reader functions, including readers that return a binary chunk in pieces. `string.dump` serializes a Lua function's prototype; it does not preserve captured upvalue values. The resulting string holds raw bytes in both string modes. Use byte-mode streams when writing it from Lua, or `LuaByteString.ToBytes` when writing it from C#. UTF-16 streams encode text as UTF-8 and cannot preserve arbitrary binary strings.
777+
778+
`Prototype.StringMode` records its string representation, so `ToBytecode` and `WriteBytecode` preserve byte-mode constants. `Prototype.FromBytecode(bytes, name, stringMode)` selects the representation when loading without a state; `LuaState.Load` uses the state's mode. Bytecode uses the Lua 5.2 function-header layout. Regenerate older Lua-CSharp chunks that used a different field order.
779+
776780
### Garbage Collection
777781

778782
Since Lua-CSharp is implemented in C#, it relies on .NET's garbage collector. As a result, memory management behavior differs from regular Lua.

‎README_JA.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,10 @@ print(l)
667667

668668
Stringライブラリの関数はすべて文字列をUTF-16として扱う実装に変更されていることに注意してください。
669669

670+
`load`は文字列とreader関数に対応し、バイナリチャンクを分割して返すreaderも利用できます。`string.dump`はLua関数のプロトタイプを保存しますが、キャプチャしたupvalueの値は保存しません。戻り値はどちらの文字列モードでも生のバイト列です。Luaからファイルに書く場合はバイトモードのストリームを、C#から書く場合は`LuaByteString.ToBytes`を使用してください。UTF-16モードのストリームはUTF-8でテキストを書き込むため、任意のバイナリ文字列を保持できません。
671+
672+
`Prototype.StringMode`は文字列表現を保持し、`ToBytecode`と`WriteBytecode`でもバイトモードの定数を保持します。stateを使わずに読み込む場合は`Prototype.FromBytecode(bytes, name, stringMode)`で表現を指定できます。`LuaState.Load`はstateのモードを使用します。バイトコードはLua 5.2の関数ヘッダー順に従います。異なる順序で生成された古いLua-CSharpのチャンクは再生成してください。
673+
670674
### ガベージコレクション
671675

672676
Lua-CSharpはC#で実装されているため.NETのGCに依存しています。そのため、メモリ管理に関する動作が通常のLuaとは異なります。
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using Lua;
2+
using Lua.Platforms;
3+
using Lua.Standard;
4+
using NuLua;
5+
using NuLua.Lua52;
6+
using LuaFunction = Lua.LuaFunction;
7+
using LuaTable = Lua.LuaTable;
8+
9+
static class BytecodeInteropVerification
10+
{
11+
static readonly ComparisonCase Closures = new(
12+
"dump-closures",
13+
"Bytecode",
14+
"""
15+
local value=10
16+
local function make(step)
17+
return function(...)
18+
value=value+step
19+
local first=...
20+
return value+select('#',...)+first
21+
end
22+
end
23+
local a,b=make(3),make(5)
24+
assert(a(4)==18 and b(7,8)==27)
25+
return a(20)
26+
""",
27+
42
28+
);
29+
30+
static readonly ComparisonCase BinaryStrings = new(
31+
"dump-binary-strings",
32+
"Bytecode",
33+
"""
34+
local bytes='\0\128\255'
35+
local utf8='\225\186\191'
36+
local function check()
37+
assert(#bytes==3 and bytes:byte(1)==0 and bytes:byte(2)==128 and bytes:byte(3)==255)
38+
assert(#utf8==3 and utf8:byte(1)==225 and utf8:byte(2)==186 and utf8:byte(3)==191)
39+
assert(''==string.sub(bytes,1,0))
40+
assert(true and not false and nil==nil)
41+
return 42
42+
end
43+
return check()
44+
""",
45+
42
46+
);
47+
48+
public static async Task VerifyAsync()
49+
{
50+
var buffer = new byte[1024 * 1024];
51+
foreach (
52+
var workload in ComparisonWorkloads
53+
.Cases.Concat(ComparisonWorkloads.CompilationCases)
54+
.Append(Closures)
55+
.Append(BinaryStrings)
56+
.DistinctBy(c => c.Name)
57+
)
58+
{
59+
using var native = Lua52State.Create();
60+
native.OpenLibraries();
61+
native.RegisterFunction(
62+
"host_add",
63+
(vm, args) =>
64+
{
65+
vm.PushNumber(args[0].Read<double>() + args[1].Read<double>());
66+
return 1;
67+
}
68+
);
69+
native.LoadString(workload.Source, "@bytecode");
70+
if (!native.TryDump(1, false, buffer, out var written))
71+
throw new InvalidOperationException(
72+
$"Native dump exceeds verification buffer: {workload.Name}"
73+
);
74+
native.SetTop(0);
75+
76+
foreach (var mode in new[] { LuaStringMode.Utf16, LuaStringMode.Bytes })
77+
{
78+
// Arbitrary bytes have the same semantics as C Lua in byte mode.
79+
if (workload == BinaryStrings && mode != LuaStringMode.Bytes)
80+
continue;
81+
using var state = LuaState.Create(
82+
mode == LuaStringMode.Bytes
83+
? LuaPlatform.DefaultByteStrings
84+
: LuaPlatform.Default
85+
);
86+
state.OpenStandardLibraries();
87+
state.Environment["host_add"] = new LuaFunction(
88+
(context, _) =>
89+
new(
90+
context.Return(
91+
context.GetArgument<double>(0) + context.GetArgument<double>(1)
92+
)
93+
)
94+
);
95+
96+
var loaded = state.Load(buffer.AsSpan(0, written), "native", "b");
97+
var results = await state.ExecuteAsync(loaded);
98+
ComparisonWorkloads.Check(
99+
workload,
100+
$"NuLua dump -> Lua-CSharp {mode}",
101+
results.Single().Read<double>()
102+
);
103+
104+
var original = state.Load(workload.Source, "@bytecode");
105+
var dump = state.Environment["string"].Read<LuaTable>()["dump"].Read<LuaFunction>();
106+
var dumped = (await state.CallAsync(dump, [original])).Single().Read<string>();
107+
native.LoadString("return assert(load(...,'managed','b'))()", "@bytecode");
108+
native.PushString(LuaByteString.ToBytes(dumped));
109+
native.Call(1, 1);
110+
ComparisonWorkloads.Check(
111+
workload,
112+
$"Lua-CSharp {mode} dump -> NuLua",
113+
native.ToNumber(-1)
114+
);
115+
native.SetTop(0);
116+
}
117+
Console.WriteLine($"PASS Bytecode {workload.Name, -20} NuLua <-> Lua-CSharp");
118+
}
119+
}
120+
}

‎sandbox/Benchmark/Program.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
$"NuLua Lua52 assembly: {typeof(NuLua.Lua52.Lua52State).Assembly.GetName().Version}"
1717
);
1818
InterpreterComparison.Verify();
19+
await BytecodeInteropVerification.VerifyAsync();
1920
return;
2021
}
2122

‎src/Lua/CodeAnalysis/Compilation/Dump.cs‎

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Runtime.InteropServices;
66
using System.Text;
77
using Lua.Internal;
8+
using Lua.IO;
89
using Lua.Runtime;
910

1011
namespace Lua.CodeAnalysis.Compilation;
@@ -96,8 +97,14 @@ public void Validate(ReadOnlySpan<char> name)
9697
}
9798
}
9899

99-
unsafe ref struct DumpState(IBufferWriter<byte> writer, bool reversedEndian)
100+
unsafe ref struct DumpState(
101+
IBufferWriter<byte> writer,
102+
bool reversedEndian,
103+
LuaStringMode stringMode
104+
)
100105
{
106+
readonly Encoding encoding =
107+
stringMode == LuaStringMode.Bytes ? LuaByteEncoding.Instance : Encoding.UTF8;
101108
public readonly IBufferWriter<byte> Writer = writer;
102109
Span<byte> unWritten;
103110

@@ -142,8 +149,8 @@ void DumpFunction(Prototype prototype)
142149
WriteInt(prototype.LineDefined); // 4
143150
WriteInt(prototype.LastLineDefined); // 4
144151
WriteByte((byte)prototype.ParameterCount); // 1
145-
WriteByte((byte)prototype.MaxStackSize); // 1
146152
WriteByte((byte)(prototype.HasVariableArguments ? 1 : 0)); // 1
153+
WriteByte((byte)prototype.MaxStackSize); // 1
147154
WriteIntSpanWithLength(MemoryMarshal.Cast<Instruction, int>(prototype.Code)); // 4
148155
WriteConstants(prototype.Constants); // 4
149156
WritePrototypes(prototype.ChildPrototypes); // 4
@@ -215,12 +222,8 @@ void WriteBool(bool v)
215222

216223
void WriteString(string v)
217224
{
218-
var bytes = Encoding.UTF8.GetBytes(v);
219-
var len = bytes.Length;
220-
if (bytes.Length != 0)
221-
{
222-
len++;
223-
}
225+
var bytes = encoding.GetBytes(v);
226+
var len = bytes.Length + 1;
224227

225228
if (sizeof(IntPtr) == 8)
226229
{
@@ -231,32 +234,38 @@ void WriteString(string v)
231234
WriteInt(len);
232235
}
233236

234-
if (len != 0)
235-
{
236-
Write(bytes);
237-
WriteByte(0);
238-
}
237+
Write(bytes);
238+
WriteByte(0);
239239
}
240240

241241
void WriteConstants(ReadOnlySpan<LuaValue> constants)
242242
{
243243
WriteInt(constants.Length);
244244
foreach (var c in constants)
245245
{
246-
WriteByte((byte)c.Type);
246+
// Lua bytecode tags differ from LuaValueType for strings.
247247
switch (c.Type)
248248
{
249249
case LuaValueType.Nil:
250+
WriteByte(0);
250251
break;
251252
case LuaValueType.Boolean:
253+
WriteByte(1);
252254
WriteBool(c.UnsafeReadDouble() != 0);
253255
break;
254256
case LuaValueType.Number:
257+
WriteByte(3);
255258
WriteDouble(c.UnsafeReadDouble());
256259
break;
257260
case LuaValueType.String:
261+
WriteByte(4);
258262
WriteString(c.UnsafeRead<string>());
259263
break;
264+
default:
265+
throw new ArgumentException(
266+
"Unsupported bytecode constant type",
267+
nameof(constants)
268+
);
260269
}
261270
}
262271
}
@@ -295,9 +304,12 @@ void WriteUpValues(ReadOnlySpan<UpValueDesc> upValues)
295304
unsafe ref struct UndumpState(
296305
ReadOnlySpan<byte> span,
297306
ReadOnlySpan<char> name,
298-
StringInternPool internPool
307+
StringInternPool internPool,
308+
LuaStringMode stringMode
299309
)
300310
{
311+
readonly Encoding encoding =
312+
stringMode == LuaStringMode.Bytes ? LuaByteEncoding.Instance : Encoding.UTF8;
301313
public ReadOnlySpan<byte> Unread = span;
302314
bool otherEndian;
303315
int pointerSize;
@@ -412,8 +424,8 @@ Prototype UndumpFunction(int depth)
412424
var lineDefined = ReadInt(); // 4
413425
var lastLineDefined = ReadInt(); // 4
414426
var parameterCount = ReadByte(); // 1
415-
var maxStackSize = ReadByte(); // 1
416427
var isVarArg = ReadBool(); // 1
428+
var maxStackSize = ReadByte(); // 1
417429
var codeLength = ReadCount(sizeof(int));
418430
var code = new Instruction[codeLength];
419431
ReadInToIntSpan(MemoryMarshal.Cast<Instruction, int>(code));
@@ -422,7 +434,7 @@ Prototype UndumpFunction(int depth)
422434
var upValues = ReadUpValues();
423435

424436
// Debug
425-
var source = ReadString();
437+
var source = ReadString("=?");
426438
var lineInfoLength = ReadCount(sizeof(int));
427439
if (lineInfoLength != 0 && lineInfoLength != codeLength)
428440
Throw("invalid line info in");
@@ -450,7 +462,10 @@ Prototype UndumpFunction(int depth)
450462
lineInfo,
451463
localVariables,
452464
upValues
453-
);
465+
)
466+
{
467+
StringMode = stringMode,
468+
};
454469
new BytecodeVerifier(prototype, name).Verify();
455470
return prototype;
456471
}
@@ -465,12 +480,12 @@ void ReadInToIntSpan(Span<int> toWrite)
465480
}
466481
}
467482

468-
string ReadString()
483+
string ReadString(string nullValue = "")
469484
{
470485
var serializedLength = pointerSize == 4 ? ReadInt() : ReadLong();
471486
if (serializedLength == 0)
472487
{
473-
return "";
488+
return nullValue;
474489
}
475490

476491
if (serializedLength < 0 || serializedLength > Unread.Length)
@@ -489,7 +504,7 @@ string ReadString()
489504
len <= 128
490505
? stackalloc char[len]
491506
: (charArrayPooled = ArrayPool<char>.Shared.Rent(len));
492-
var count = Encoding.UTF8.GetChars(bytes, chars);
507+
var count = encoding.GetChars(bytes, chars);
493508
return internPool.Intern(chars[..count]);
494509
}
495510
finally
@@ -507,18 +522,18 @@ LuaValue[] ReadConstants()
507522
var constants = new LuaValue[count];
508523
for (var i = 0; i < count; i++)
509524
{
510-
var type = (LuaValueType)ReadByte();
525+
var type = ReadByte();
511526
switch (type)
512527
{
513-
case LuaValueType.Nil:
528+
case 0:
514529
break;
515-
case LuaValueType.Boolean:
530+
case 1:
516531
constants[i] = ReadBool();
517532
break;
518-
case LuaValueType.Number:
533+
case 3:
519534
constants[i] = ReadDouble();
520535
break;
521-
case LuaValueType.String:
536+
case 4:
522537
constants[i] = ReadString();
523538
break;
524539
default:

‎src/Lua/CodeAnalysis/Compilation/Parser.cs‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -979,7 +979,7 @@ public static Prototype Parse(LuaState l, TextReader r, string name)
979979
f.Proto.IsVarArg = true;
980980
f.Proto.LineDefined = 0;
981981
p.MainFunction();
982-
var prototype = f.Proto.CreatePrototypeAndRelease();
982+
var prototype = f.Proto.CreatePrototypeAndRelease(l.Platform.StringMode);
983983
f.Release();
984984
return prototype;
985985
}
@@ -990,7 +990,11 @@ public static void Dump(
990990
bool useLittleEndian = true
991991
)
992992
{
993-
DumpState state = new(writer, useLittleEndian ^ BitConverter.IsLittleEndian);
993+
DumpState state = new(
994+
writer,
995+
useLittleEndian ^ BitConverter.IsLittleEndian,
996+
prototype.StringMode
997+
);
994998
state.Dump(prototype);
995999
}
9961000

@@ -1001,7 +1005,11 @@ public static byte[] Dump(Prototype prototype, bool useLittleEndian = true)
10011005
return writer.WrittenSpan.ToArray();
10021006
}
10031007

1004-
public static Prototype Undump(ReadOnlySpan<byte> span, ReadOnlySpan<char> name)
1008+
public static Prototype Undump(
1009+
ReadOnlySpan<byte> span,
1010+
ReadOnlySpan<char> name,
1011+
LuaStringMode stringMode = LuaStringMode.Utf16
1012+
)
10051013
{
10061014
if (name.Length > 0)
10071015
{
@@ -1014,7 +1022,7 @@ public static Prototype Undump(ReadOnlySpan<byte> span, ReadOnlySpan<char> name)
10141022
}
10151023

10161024
using var internPool = new StringInternPool(4);
1017-
UndumpState state = new(span, name, internPool);
1025+
UndumpState state = new(span, name, internPool, stringMode);
10181026
return state.Undump();
10191027
}
10201028
}

0 commit comments

Comments
 (0)