Skip to content

Commit e06e761

Browse files
committed
Pass strings to and from Lua as byte arrays, encode/decode only when necessary (fixes #6)
1 parent 1c4a1f0 commit e06e761

File tree

6 files changed

+198
-32
lines changed

6 files changed

+198
-32
lines changed

Eluant.Tests/Runtime.cs

+33
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,39 @@ public void ReferenceTableRewriteDoesNotConfuseReferences()
9090
}
9191
}
9292

93+
[Test]
94+
public void NonAsciiStringMarshallingFromLua()
95+
{
96+
using (var runtime = new LuaRuntime()) {
97+
runtime.DoString(@"k = string.char(0xFC, 0xFF)").Dispose();
98+
99+
LuaString clrString;
100+
using (var k = runtime.Globals["k"]) {
101+
clrString = (LuaString)k;
102+
}
103+
104+
Assert.AreEqual(2, clrString.AsByteArray().Length, "clrString.AsByteArray().Length");
105+
106+
runtime.Globals["kclr"] = clrString;
107+
108+
runtime.DoString("assert(k == kclr, 'k == kclr')").Dispose();
109+
}
110+
}
111+
112+
[Test]
113+
public void NonAsciiStringMarshallingFromCLR()
114+
{
115+
var str = "a\u1234b";
116+
117+
Assert.AreEqual(3, str.Length, "str.Length");
118+
119+
using (var runtime = new LuaRuntime()) {
120+
runtime.Globals["s"] = str;
121+
122+
Assert.AreEqual(str, runtime.Globals["s"].ToString(), "str == s");
123+
}
124+
}
125+
93126
// This may seem like an unnecessary test, but it is actualy required to make sure that Lua runtimes can be
94127
// properly finalized when there are no outstanding managed references to them. If we ever make the mistake of
95128
// passing something into the Lua runtime that represents a strong reference that the runtime is reachable from,

Eluant/ByteArrayEqualityComparer.cs

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//
2+
// ByteArrayEqualityComparer.cs
3+
//
4+
// Author:
5+
// Chris Howie <[email protected]>
6+
//
7+
// Copyright (c) 2015 Chris Howie
8+
//
9+
// Permission is hereby granted, free of charge, to any person obtaining a copy
10+
// of this software and associated documentation files (the "Software"), to deal
11+
// in the Software without restriction, including without limitation the rights
12+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
// copies of the Software, and to permit persons to whom the Software is
14+
// furnished to do so, subject to the following conditions:
15+
//
16+
// The above copyright notice and this permission notice shall be included in
17+
// all copies or substantial portions of the Software.
18+
//
19+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
// THE SOFTWARE.
26+
using System;
27+
using System.Collections.Generic;
28+
using System.Linq;
29+
30+
namespace Eluant
31+
{
32+
internal class ByteArrayEqualityComparer : IEqualityComparer<byte[]>
33+
{
34+
public static readonly ByteArrayEqualityComparer Instance = new ByteArrayEqualityComparer();
35+
36+
public ByteArrayEqualityComparer() { }
37+
38+
#region IEqualityComparer implementation
39+
40+
public bool Equals(byte[] x, byte[] y)
41+
{
42+
if (x == null) {
43+
return y == null;
44+
}
45+
46+
if (y == null || x.Length != y.Length) {
47+
return false;
48+
}
49+
50+
return x.SequenceEqual(y);
51+
}
52+
53+
public int GetHashCode(byte[] obj)
54+
{
55+
if (obj == null) {
56+
throw new ArgumentNullException("obj");
57+
}
58+
59+
int code = 0;
60+
61+
foreach (byte b in obj) {
62+
code = (code >> 29) | (code << 3) ^ b;
63+
}
64+
65+
return code;
66+
}
67+
68+
#endregion
69+
}
70+
}
71+

Eluant/Eluant.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
<Compile Include="LuaClrObjectReference.cs" />
7575
<Compile Include="ObjectBinding\BasicLuaBinder.cs" />
7676
<Compile Include="ObjectBinding\IBindingContext.cs" />
77+
<Compile Include="ByteArrayEqualityComparer.cs" />
7778
</ItemGroup>
7879
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
7980
<ItemGroup>

Eluant/LuaApi.cs

+31-4
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,31 @@ public static void lua_pushcfunction(IntPtr L, lua_CFunction f)
167167
public static extern void lua_pushlightuserdata(IntPtr L, IntPtr p);
168168

169169
[DllImport(LUA_DLL, CallingConvention=LUA_CALLING_CONVENTION)]
170-
public static extern void lua_pushlstring(IntPtr L, [MarshalAs(UnmanagedType.LPStr)] string s, UIntPtr len);
170+
public static extern void lua_pushlstring(IntPtr L, byte[] s, UIntPtr len);
171+
172+
public static void lua_pushlstring(IntPtr L, string s, int len)
173+
{
174+
if (s == null) {
175+
lua_pushnil(L);
176+
} else {
177+
var ulen = new UIntPtr(checked((ulong)len));
178+
179+
var buffer = new byte[len];
180+
181+
for (int i = 0; i < s.Length; ++i) {
182+
buffer[i] = unchecked((byte)s[i]);
183+
}
184+
185+
lua_pushlstring(L, buffer, ulen);
186+
}
187+
}
171188

172189
public static void lua_pushstring(IntPtr L, string s)
173190
{
174191
if (s == null) {
175192
lua_pushnil(L);
176193
} else {
177-
lua_pushlstring(L, s, new UIntPtr(unchecked((ulong)s.Length)));
194+
lua_pushlstring(L, s, s.Length);
178195
}
179196
}
180197

@@ -252,7 +269,7 @@ public static void lua_setglobal(IntPtr L, string name)
252269
[DllImport(LUA_DLL, CallingConvention=LUA_CALLING_CONVENTION)]
253270
public static extern IntPtr lua_tolstring(IntPtr L, int index, ref UIntPtr len);
254271

255-
public static string lua_tostring(IntPtr L, int index)
272+
public static byte[] lua_tostring(IntPtr L, int index)
256273
{
257274
UIntPtr len = UIntPtr.Zero;
258275

@@ -261,7 +278,17 @@ public static string lua_tostring(IntPtr L, int index)
261278
return null;
262279
}
263280

264-
return Marshal.PtrToStringAnsi(stringPtr, checked((int)len.ToUInt32()));
281+
var len32 = checked((int)len.ToUInt32());
282+
283+
if (len32 == 0) {
284+
return new byte[0];
285+
}
286+
287+
var buffer = new byte[len32];
288+
289+
Marshal.Copy(stringPtr, buffer, 0, len32);
290+
291+
return buffer;
265292
}
266293

267294
[DllImport(LUA_DLL, CallingConvention=LUA_CALLING_CONVENTION)]

Eluant/LuaRuntime.cs

+9-9
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ internal LuaValue Wrap(int index)
423423
return (LuaNumber)LuaApi.lua_tonumber(LuaState, index);
424424

425425
case LuaApi.LuaType.String:
426-
return (LuaString)LuaApi.lua_tostring(LuaState, index);
426+
return new LuaString(LuaApi.lua_tostring(LuaState, index));
427427

428428
case LuaApi.LuaType.Table:
429429
return new LuaTable(this, CreateReference(index));
@@ -495,7 +495,7 @@ private void ProcessReleasedReferences()
495495
private void LoadString(string str)
496496
{
497497
if (LuaApi.luaL_loadstring(LuaState, str) != 0) {
498-
var error = LuaApi.lua_tostring(LuaState, -1);
498+
var error = new LuaString(LuaApi.lua_tostring(LuaState, -1));
499499
LuaApi.lua_pop(LuaState, 1);
500500

501501
throw new LuaException(error);
@@ -575,7 +575,7 @@ private LuaVararg Call(IList<LuaValue> args)
575575
OnEnterClr();
576576

577577
// Finally block will take care of popping the error message.
578-
throw new LuaException(LuaApi.lua_tostring(LuaState, -1));
578+
throw new LuaException(new LuaString(LuaApi.lua_tostring(LuaState, -1)));
579579
}
580580
needEnterClr = false;
581581
OnEnterClr();
@@ -766,7 +766,7 @@ internal void PushCustomClrObject(LuaClrObjectValue obj)
766766
.SelectMany(iface => iface.GetCustomAttributes(typeof(MetamethodAttribute), false).Cast<MetamethodAttribute>());
767767

768768
foreach (var metamethod in metamethods) {
769-
LuaApi.lua_pushstring(LuaState, metamethod.MethodName);
769+
new LuaString(metamethod.MethodName).Push(this);
770770
Push(metamethodCallbacks[metamethod.MethodName]);
771771
LuaApi.lua_settable(LuaState, -3);
772772
}
@@ -1106,13 +1106,13 @@ private int LuaToClrBoundary(IntPtr state, LuaToClrBoundaryCallback callback)
11061106
LuaApi.lua_settop(state, oldTop);
11071107

11081108
LuaApi.lua_pushboolean(LuaState, 0);
1109-
LuaApi.lua_pushstring(LuaState, ex.Message);
1109+
new LuaString(ex.Message).Push(this);
11101110
return 2;
11111111
} catch (Exception ex) {
11121112
LuaApi.lua_settop(state, oldTop);
11131113

11141114
LuaApi.lua_pushboolean(state, 0);
1115-
LuaApi.lua_pushstring(state, "Uncaught CLR exception at Lua->CLR boundary: " + ex.ToString());
1115+
new LuaString("Uncaught CLR exception at Lua->CLR boundary: " + ex.ToString()).Push(this);
11161116
return 2;
11171117
} finally {
11181118
try {
@@ -1228,7 +1228,7 @@ private int MakeManagedCall(IntPtr state, MethodWrapper wrapper)
12281228
throw new LuaException(string.Format("Argument {0}: Cannot be a string.", i + 1));
12291229
}
12301230

1231-
args[i] = LuaApi.lua_tostring(state, i + 1);
1231+
args[i] = new LuaString(LuaApi.lua_tostring(state, i + 1)).Value;
12321232
break;
12331233

12341234
case LuaApi.LuaType.Table:
@@ -1338,11 +1338,11 @@ private int MakeManagedCall(IntPtr state, MethodWrapper wrapper)
13381338
return 2;
13391339
} catch (LuaException ex) {
13401340
LuaApi.lua_pushboolean(state, 0);
1341-
LuaApi.lua_pushstring(state, ex.Message);
1341+
new LuaString(ex.Message).Push(this);
13421342
return 2;
13431343
} catch (Exception ex) {
13441344
LuaApi.lua_pushboolean(state, 0);
1345-
LuaApi.lua_pushstring(state, "Uncaught CLR exception at Lua->CLR boundary: " + ex.ToString());
1345+
new LuaString("Uncaught CLR exception at Lua->CLR boundary: " + ex.ToString()).Push(this);
13461346
return 2;
13471347
} finally {
13481348
// Dispose whatever we need to. It's okay to dispose result objects, as that will only release the CLR

Eluant/LuaString.cs

+53-19
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,47 @@
2525
// THE SOFTWARE.
2626

2727
using System;
28+
using System.Text;
29+
using System.Collections;
2830

2931
namespace Eluant
3032
{
3133
public sealed class LuaString : LuaValueType,
3234
IEquatable<LuaString>, IEquatable<string>,
3335
IComparable, IComparable<LuaString>, IComparable<string>
3436
{
35-
public string Value { get; private set; }
37+
private string stringValue;
38+
private byte[] byteValue;
39+
40+
public string Value
41+
{
42+
get {
43+
if (stringValue == null) {
44+
stringValue = Encoding.UTF8.GetString(byteValue);
45+
}
46+
47+
return stringValue;
48+
}
49+
}
3650

3751
public LuaString(string value)
3852
{
3953
if (value == null) { throw new ArgumentNullException("value"); }
4054

41-
Value = value;
55+
stringValue = value;
56+
byteValue = Encoding.UTF8.GetBytes(value);
57+
}
58+
59+
public LuaString(byte[] value)
60+
{
61+
if (value == null) { throw new ArgumentNullException("value"); }
62+
63+
byteValue = (byte[])value.Clone();
64+
}
65+
66+
public byte[] AsByteArray()
67+
{
68+
return (byte[])byteValue.Clone();
4269
}
4370

4471
public override bool ToBoolean()
@@ -67,6 +94,10 @@ internal override object ToClrType(Type type)
6794
return Value;
6895
}
6996

97+
if (type == typeof(byte[])) {
98+
return AsByteArray();
99+
}
100+
70101
return base.ToClrType(type);
71102
}
72103

@@ -77,7 +108,7 @@ public override bool Equals(LuaValue other)
77108

78109
internal override void Push(LuaRuntime runtime)
79110
{
80-
LuaApi.lua_pushlstring(runtime.LuaState, Value, new UIntPtr((ulong)Value.Length));
111+
LuaApi.lua_pushlstring(runtime.LuaState, byteValue, new UIntPtr(checked((uint)byteValue.Length)));
81112
}
82113

83114
public static implicit operator LuaString(string v)
@@ -90,9 +121,15 @@ public static implicit operator string(LuaString s)
90121
return object.ReferenceEquals(s, null) ? null : s.Value;
91122
}
92123

124+
private int? hashCode = null;
125+
93126
public override int GetHashCode()
94127
{
95-
return Value.GetHashCode();
128+
if (!hashCode.HasValue) {
129+
hashCode = ByteArrayEqualityComparer.Instance.GetHashCode(byteValue);
130+
}
131+
132+
return hashCode.Value;
96133
}
97134

98135
public override bool Equals(object obj)
@@ -105,7 +142,7 @@ public bool Equals(LuaString obj)
105142
if (object.ReferenceEquals(obj, this)) { return true; }
106143
if (object.ReferenceEquals(obj, null)) { return false; }
107144

108-
return obj.Value == Value;
145+
return ByteArrayEqualityComparer.Instance.Equals(byteValue, obj.byteValue);
109146
}
110147

111148
public bool Equals(string obj)
@@ -116,27 +153,24 @@ public bool Equals(string obj)
116153
}
117154

118155
// No (LuaString, LuaString) overload. With implicit conversion to string, that creates ambiguity.
119-
120-
public static bool operator==(LuaString a, string b)
156+
public static bool operator==(LuaString a, LuaString b)
121157
{
122-
return (string)a == b;
158+
if (object.ReferenceEquals(a, null)) {
159+
return object.ReferenceEquals(b, null);
160+
}
161+
162+
if (object.ReferenceEquals(b, null)) {
163+
return false;
164+
}
165+
166+
return a.Equals(b);
123167
}
124168

125-
public static bool operator!=(LuaString a, string b)
169+
public static bool operator!=(LuaString a, LuaString b)
126170
{
127171
return !(a == b);
128172
}
129173

130-
// public static bool operator==(string a, LuaString b)
131-
// {
132-
// return a == (string)b;
133-
// }
134-
//
135-
// public static bool operator!=(string a, LuaString b)
136-
// {
137-
// return !(a == b);
138-
// }
139-
140174
public int CompareTo(LuaString s)
141175
{
142176
return CompareTo(object.ReferenceEquals(s, null) ? null : s.Value);

0 commit comments

Comments
 (0)