Skip to content

Commit

Permalink
Merge pull request #167 from gudzpoz/int64
Browse files Browse the repository at this point in the history
Support 64-bit integers across Lua-Java boundaries
  • Loading branch information
gudzpoz authored Apr 26, 2024
2 parents 7bd2822 + 3be0edc commit a7faf5e
Show file tree
Hide file tree
Showing 23 changed files with 1,304 additions and 758 deletions.
26 changes: 26 additions & 0 deletions docs/conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,29 @@ When calling Java methods from Lua, we `SEMI`-convert the return value. Currentl
::: warning
Currently, you cannot convert a C closure back to a `JFunction`, even if the closure simply wraps around `JFunction`.
:::

## 64-Bit Integers

To ensure compatibility across Lua versions, this library uses `double` for most numbers.
However, [Lua 5.3](https://www.lua.org/manual/5.3/manual.html#8.1) introduced an integer subtype
for numbers, which allows usage of 64-bit integers in Lua (on 64-bit machines mostly).
This library ensures that no truncation ever happens when casting between `long` and `double`
(which can happen on 32-bit machines where `long` values get truncated to 32-bit Lua integers).
To retrieve or push integer values that exceed the safe integer range of `double` numbers,
you will need to use
[`party.iroiro.luajava.Lua#push(long)`](./javadoc/party/iroiro/luajava/Lua.html#push(long))
and
[`party.iroiro.luajava.Lua#toInteger`](./javadoc/party/iroiro/luajava/Lua.html#toInteger(int)).

Also, when passing values via proxies or Java calls on the Lua side,
the values will get auto converted to ensure maximal precision.
For example, the following Lua snippet passes `2^60 + 1` around correctly
(which cannot fit into a `double`) when running with 64-bit Lua 5.3:

```lua
POW_2_60 = 1152921504606846976
Long = java.import('java.lang.Long')
l = Long(POW_2_60 + 1)
assert(l:toString() == "1152921504606846977")
assert(l:longValue() == 1152921504606846977)
```
2 changes: 1 addition & 1 deletion example/src/test/java/party/iroiro/luajava/JuaApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private void convertTableTest(Lua L) {

private static class NativeTest extends Lua51Natives {
@Override
public long lua_newuserdata(long ptr, int size) {
public long lua_newuserdata(long ptr, long size) {
return super.lua_newuserdata(ptr, size);
}
}
Expand Down
187 changes: 164 additions & 23 deletions example/suite/src/main/java/party/iroiro/luajava/LuaTestSuite.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
import static party.iroiro.luajava.Lua.LuaType.*;

public class LuaTestSuite<T extends AbstractLua> {

public static final long I_60_BITS = 1152921504606846976L;

@SuppressWarnings("UnusedReturnValue")
public static <S> S assertInstanceOf(Class<S> sClass, Object o) {
assertTrue(sClass.isInstance(o));
Expand All @@ -43,6 +46,7 @@ public LuaTestSuite(T L, LuaTestSupplier<T> constructor) {
public void test() {
L.openLibraries();
LuaScriptSuite.addAssertThrows(L);
test64BitInteger();
testDump();
testException();
testExternalLoader();
Expand All @@ -65,6 +69,122 @@ public void test() {
testThreads();
}

private void test64BitInteger() {
try (T L = constructor.get()) {
L.push((Number) I_60_BITS);
assertNotEquals(0, L.toInteger(-1));
assertNotEquals(0, L.toNumber(-1), 1);

/*
* Although I expected casting (long) to (double) should be a reproducible operation,
* apparently it doesn't on some platforms, including the Android AVD emulator.
* We use `approx` (below) to handle possible double comparisons.
*/
assertEquals(OK, L.run(
"function approx(a, b)\n" +
"if a == b then\n" +
" return true\n" +
"end\n" +
"local offset = a / b - 1\n" +
"offset = offset < 0 and -offset or offset\n" +
"return offset < 0.000001\n" +
"end"));

assertEquals(OK, L.run("return _VERSION"));
String version = L.toString(-1);
assertEquals(OK, L.run("pow_2_60 = 1152921504606846976\nreturn pow_2_60 ~= pow_2_60 + 1"));
boolean supports64BitInteger = L.toBoolean(-1);
assertEquals(OK, L.run("double_int = 1099511627776\nreturn double_int ~= double_int + 1"));
boolean supportsDouble = L.toBoolean(-1); // double_int = 2 ^ 40.
if (!supportsDouble) {
assertFalse(supports64BitInteger);
return;
}

L.push(I_60_BITS);
boolean truncatesTo32Bit = L.toInteger(-1) == 0;
assertFalse(truncatesTo32Bit);

/* Things seem rather complicated:
* - (64-bit machine + Lua 5.1 ~ 5.2): L.push(I_60_BITS) -> an approximated double value
* - (64-bit machine + Lua 5.3 ~ 5.4): L.push(I_60_BITS) -> exact integer value
* - (32-bit machine + Lua 5.1 ~ 5.2): L.push(I_60_BITS) -> truncates to int (0), then to double (0)
* - (32-bit machine + Lua 5.3 ~ 5.4): L.push(I_60_BITS) -> truncates to int (0), then to long (0)
* All machines seem to use double.
* In the JNI code, we try to ensure that no truncation ever happens.
*/

if (version != null && (version.equals("Lua 5.4") || version.equals("Lua 5.3"))) {
assertTrue(supports64BitInteger);
}

// Since pow_2_60 contains trailing zero bits,
// for most 64-bit machines, (long) (double) pow_2_60 == pow_2_60, so we need a +1.
long i60Bits = I_60_BITS;
L.push(i60Bits);
L.setGlobal("i_from_java");
L.push(i60Bits + 1);
L.setGlobal("i_plus_from_java");
assertEquals(OK, L.run("return approx(pow_2_60, i_from_java)"));
assertTrue(L.toBoolean(-1));
assertEquals(OK, L.run("return approx(pow_2_60 + 1, i_plus_from_java)"));
assertTrue(L.toBoolean(-1));
assertEquals(OK, L.run("return approx(i_from_java, i_plus_from_java)"));
assertTrue(L.toBoolean(-1));

assertEquals(OK, L.run("return pow_2_60 + 1, i_from_java, i_plus_from_java"));
if (supports64BitInteger) {
assertFalse(L.equal(-3, -2));
assertFalse(L.equal(-2, -1));
assertTrue(L.equal(-3, -1));
}

long converted = L.toInteger(-1);
assertEquals("actual: " + converted, supports64BitInteger, converted == i60Bits + 1);
converted = L.toInteger(-3);
assertEquals("actual: " + converted, supports64BitInteger, converted == i60Bits + 1);

//noinspection Convert2Lambda
L.push(new JFunction() {
@Override
public int __call(Lua L) {
L.push(L.toInteger(-1));
return 1;
}
});
L.setGlobal("jfunc");
assertEquals(OK, L.run("return approx(pow_2_60 + 1, jfunc(pow_2_60 + 1))"));
assertTrue(L.toBoolean(-1));

L.pushJavaClass(LuaTestSuite.class);
L.setGlobal("suite");
assertEquals(OK, L.run("return approx(pow_2_60 + 1, suite:passAlong(pow_2_60 + 1))"));
assertTrue(L.toBoolean(-1));

if (supports64BitInteger) {
assertEquals(OK, L.run("return " +
"pow_2_60 + 1, jfunc(pow_2_60 + 1), suite:passAlong(pow_2_60 + 1)"));
assertTrue(L.equal(-3, -2));
assertTrue(L.equal(-2, -1));
}

assertEquals(OK, L.run("return { passAlong = function(_, l) return l end }"));
Object o = L.createProxy(new Class[]{PasserAlong.class}, FULL);
assertEquals(
supports64BitInteger,
I_60_BITS + 1 == ((PasserAlong) o).passAlong(I_60_BITS + 1)
);

if (supports64BitInteger) {
L.push(I_60_BITS + 1);
L.push(I_60_BITS + 1);
LuaValue v = L.get();
v.push();
assertTrue(L.equal(-2, -1));
}
}
}

private void testStackPositions() {
try (T L = constructor.get()) {
Random random = new Random();
Expand Down Expand Up @@ -793,21 +913,7 @@ private void testOverflow() {
L.setMetatable(-2);
L.pushValue(-1);
int ref = L.ref();
ArrayList<LuaTestConsumer<T>> stackIncrementingOperations = new ArrayList<>(Arrays.asList(
L -> L.createTable(0, 0),
L -> L.getGlobal("java"),
L -> L.refGet(ref),
L -> L.pushValue(testTableI),
L -> L.getMetatable(testTableI),
L -> L.getField(testTableI, "S"),
L -> L.rawGetI(testTableI, 1),
L -> L.getMetaField(testTableI, "F")
));
for (Object[] data : DATA) {
for (Lua.Conversion conv : Lua.Conversion.values()) {
stackIncrementingOperations.add(L -> L.push(data[4], conv));
}
}
ArrayList<LuaTestConsumer<T>> stackIncrementingOperations = getLuaTestConsumers(ref, testTableI);
for (LuaTestConsumer<T> t : stackIncrementingOperations) {
assertThrows("No more stack space available", RuntimeException.class, () -> {
double i = 1.0;
Expand All @@ -828,6 +934,25 @@ private void testOverflow() {
L.pop(1);
}

private static <T extends AbstractLua> ArrayList<LuaTestConsumer<T>> getLuaTestConsumers(int ref, int testTableI) {
ArrayList<LuaTestConsumer<T>> stackIncrementingOperations = new ArrayList<>(Arrays.asList(
L -> L.createTable(0, 0),
L -> L.getGlobal("java"),
L -> L.refGet(ref),
L -> L.pushValue(testTableI),
L -> L.getMetatable(testTableI),
L -> L.getField(testTableI, "S"),
L -> L.rawGetI(testTableI, 1),
L -> L.getMetaField(testTableI, "F")
));
for (Object[] data : DATA) {
for (Lua.Conversion conv : Lua.Conversion.values()) {
stackIncrementingOperations.add(L -> L.push(data[4], conv));
}
}
return stackIncrementingOperations;
}

private void testJavaToLuaConversions() {
Integer integer = 1 << 14;
L.push(integer, Lua.Conversion.NONE);
Expand Down Expand Up @@ -892,8 +1017,10 @@ public Verifier(LuaTestBiPredicate<Object, Object> verifier) {
}

public void verify(Lua L, Object original) {
assertTrue(original == null ? "null" : original.getClass().getName(),
verifier.test(original, L.toObject(-1)));
assertTrue(
original == null ? "null" : (original.getClass().getName() + " " + original),
verifier.test(original, L.toObject(-1))
);
}
}

Expand Down Expand Up @@ -925,15 +1052,20 @@ private static Verifier V(LuaTestBiPredicate<Object, Object> verifier) {
return true;
}
if (i instanceof Number) {
Number i1 = (Number) i;
if (o instanceof BigInteger) {
return !o.equals(i)
&& ((BigInteger) o).compareTo(
BigInteger.valueOf(((Number) i).longValue())) > 0;
} else if (o instanceof Number) {
return Math.abs(((Number) o).doubleValue() - ((Number) i).doubleValue())
< 0.00000001;
} else if (o instanceof Character) {
return ((Number) i).intValue() == (int) ((Character) o);
BigInteger.valueOf(i1.longValue())) > 0;
} else {
if (o instanceof Number) {
Number i2 = (Number) o;
return (Math.abs(i2.doubleValue() - i1.doubleValue()) < 0.00000001)
|| i2.longValue() == i1.longValue()
|| i2.intValue() == i1.intValue();
} else if (o instanceof Character) {
return i1.intValue() == (int) ((Character) o);
}
}
}
return false;
Expand Down Expand Up @@ -1011,4 +1143,13 @@ private static Verifier V(LuaTestBiPredicate<Object, Object> verifier) {
},
};

@SuppressWarnings("unused")
public static long passAlong(long value) {
return value;
}

public interface PasserAlong {
long passAlong(long value);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ private void callTest() {
int top = l.getTop();
int sum = 0;
for (int i = 1; i <= top; i++) {
sum += l.toNumber(i);
sum += (int) l.toNumber(i);
}
l.push(sum);
return 1;
Expand Down Expand Up @@ -159,7 +159,7 @@ private void equalityTest(Lua K, boolean equals) {
AbstractLuaValue<Lua> mock = new AbstractLuaValue<Lua>(L, NUMBER) {
@Override
public void push() {

L.push(2);
}

@Override
Expand All @@ -176,11 +176,6 @@ public void close() {
public Lua.LuaType type() {
return NUMBER;
}

@Override
public Lua state() {
return L;
}
};
assertNotEquals(L.from(1), mock);

Expand All @@ -195,7 +190,7 @@ public Lua state() {
AbstractLuaValue<Lua> mock1 = new AbstractLuaValue<Lua>(L, TABLE) {
@Override
public void push() {

state().push(Collections.emptyList());
}

@Override
Expand All @@ -212,13 +207,9 @@ public void close() {
public Lua.LuaType type() {
return TABLE;
}

@Override
public Lua state() {
return L;
}
};
assertNotEquals(l, mock1);
//noinspection EqualsWithItself
assertEquals(l, l);
}
}
Expand Down
12 changes: 8 additions & 4 deletions lua51/jni/mod/luacomp.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/**
* Opens individual libraries when one does not want them all
*/
static inline void luaJ_openlib_call(lua_State * L, const char *libName, lua_CFunction loader) {
static inline void luaJ_openlib_call(lua_State * L, const char * libName, lua_CFunction loader) {
lua_pushcfunction(L, loader);
lua_pushstring(L, libName);
lua_call(L, 1, 0);
Expand Down Expand Up @@ -71,7 +71,7 @@ static int luaJ_resume(lua_State * L, int narg) {
return lua_resume(L, narg);
}

static void *luaL_testudata(lua_State *L, int ud, const char *tname) {
static void *luaL_testudata(lua_State * L, int ud, const char * tname) {
void *p = lua_touserdata(L, ud);
if (p != NULL) { /* value is a userdata? */
if (lua_getmetatable(L, ud)) { /* does it have a metatable? */
Expand All @@ -89,8 +89,12 @@ static int luaJ_initloader(lua_State * L) {
return luaJ_insertloader(L, "loaders");
}

static int luaJ_dump (lua_State *L, lua_Writer writer, void *data) {
static int luaJ_dump(lua_State * L, lua_Writer writer, void * data) {
return lua_dump (L, writer, data);
}

#endif /* !LUACOMP_H */
static int luaJ_isinteger(lua_State * L, int index) {
return 0;
}

#endif /* !LUACOMP_H */
Loading

0 comments on commit a7faf5e

Please sign in to comment.