Skip to content

Commit 6702908

Browse files
authored
Fix handling of uint64_t in Embind (#24285)
Before this fix, `uint64_t` would be returned as a signed integer from Embind (e.g. if you return `UINT64_MAX`, it gets returned as `-1`). I fixed that behaviour so that unsigned integers are correctly "fixed up" like they already are for `uint32_t`, and added tests for 64-bit integer limits to `test_i64_binding`. In the process I had to also delete the invalid test `other.test_embind_long_long` - it had incorrect expectations (see commit message for more details) and is now superseded by the correct test mentioned above. - Fixes #20354. Closes #20353. - Fixes #13902.
1 parent b628df9 commit 6702908

File tree

9 files changed

+110
-147
lines changed

9 files changed

+110
-147
lines changed

src/lib/libembind.js

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -318,59 +318,51 @@ var LibraryEmbind = {
318318
}
319319
},
320320

321+
#if ASSERTIONS
322+
$assertIntegerRange__deps: ['$embindRepr'],
323+
$assertIntegerRange: (typeName, value, minRange, maxRange) => {
324+
if (value < minRange || value > maxRange) {
325+
throw new TypeError(`Passing a number "${embindRepr(value)}" from JS side to C/C++ side to an argument of type "${typeName}", which is outside the valid range [${minRange}, ${maxRange}]!`);
326+
}
327+
},
328+
#endif
329+
321330
_embind_register_integer__docs: '/** @suppress {globalThis} */',
322331
// When converting a number from JS to C++ side, the valid range of the number is
323332
// [minRange, maxRange], inclusive.
324333
_embind_register_integer__deps: [
325334
'$integerReadValueFromPointer', '$readLatin1String', '$registerType',
326335
#if ASSERTIONS
327336
'$embindRepr',
337+
'$assertIntegerRange',
328338
#endif
329339
],
330340
_embind_register_integer: (primitiveType, name, size, minRange, maxRange) => {
331341
name = readLatin1String(name);
332-
// LLVM doesn't have signed and unsigned 32-bit types, so u32 literals come
333-
// out as 'i32 -1'. Always treat those as max u32.
334-
if (maxRange === -1) {
335-
maxRange = 4294967295;
336-
}
337342

338-
var fromWireType = (value) => value;
343+
const isUnsignedType = minRange === 0;
339344

340-
if (minRange === 0) {
345+
let fromWireType = (value) => value;
346+
if (isUnsignedType) {
341347
var bitshift = 32 - 8*size;
342348
fromWireType = (value) => (value << bitshift) >>> bitshift;
349+
maxRange = fromWireType(maxRange);
343350
}
344351

345-
var isUnsignedType = (name.includes('unsigned'));
346-
var checkAssertions = (value, toTypeName) => {
352+
registerType(primitiveType, {
353+
name,
354+
'fromWireType': fromWireType,
355+
'toWireType': (destructors, value) => {
347356
#if ASSERTIONS
348-
if (typeof value != "number" && typeof value != "boolean") {
349-
throw new TypeError(`Cannot convert "${embindRepr(value)}" to ${toTypeName}`);
350-
}
351-
if (value < minRange || value > maxRange) {
352-
throw new TypeError(`Passing a number "${embindRepr(value)}" from JS side to C/C++ side to an argument of type "${name}", which is outside the valid range [${minRange}, ${maxRange}]!`);
353-
}
354-
#endif
355-
}
356-
var toWireType;
357-
if (isUnsignedType) {
358-
toWireType = function(destructors, value) {
359-
checkAssertions(value, this.name);
360-
return value >>> 0;
361-
}
362-
} else {
363-
toWireType = function(destructors, value) {
364-
checkAssertions(value, this.name);
357+
if (typeof value != "number" && typeof value != "boolean") {
358+
throw new TypeError(`Cannot convert "${embindRepr(value)}" to ${name}`);
359+
}
360+
assertIntegerRange(name, value, minRange, maxRange);
361+
#endif
365362
// The VM will perform JS to Wasm value conversion, according to the spec:
366363
// https://www.w3.org/TR/wasm-js-api-1/#towebassemblyvalue
367364
return value;
368-
}
369-
}
370-
registerType(primitiveType, {
371-
name,
372-
'fromWireType': fromWireType,
373-
'toWireType': toWireType,
365+
},
374366
argPackAdvance: GenericWireTypeSize,
375367
'readValueFromPointer': integerReadValueFromPointer(name, size, minRange !== 0),
376368
destructorFunction: null, // This type does not need a destructor
@@ -380,31 +372,46 @@ var LibraryEmbind = {
380372
#if WASM_BIGINT
381373
_embind_register_bigint__docs: '/** @suppress {globalThis} */',
382374
_embind_register_bigint__deps: [
383-
'$embindRepr', '$readLatin1String', '$registerType', '$integerReadValueFromPointer'],
375+
'$readLatin1String', '$registerType', '$integerReadValueFromPointer',
376+
#if ASSERTIONS
377+
'$embindRepr',
378+
'$assertIntegerRange',
379+
#endif
380+
],
384381
_embind_register_bigint: (primitiveType, name, size, minRange, maxRange) => {
385382
name = readLatin1String(name);
386383

387-
var isUnsignedType = (name.indexOf('u') != -1);
384+
const isUnsignedType = minRange === 0n;
388385

389-
// maxRange comes through as -1 for uint64_t (see issue 13902). Work around that temporarily
386+
let fromWireType = (value) => value;
390387
if (isUnsignedType) {
391-
maxRange = (1n << 64n) - 1n;
388+
// uint64 get converted to int64 in ABI, fix them up like we do for 32-bit integers.
389+
const bitSize = size * 8;
390+
fromWireType = (value) => {
391+
#if MEMORY64
392+
// FIXME(https://github.com/emscripten-core/emscripten/issues/16975)
393+
// `size_t` ends up here, but it's transferred in the ABI as a plain number instead of a bigint.
394+
if (typeof value == 'number') {
395+
return value >>> 0;
396+
}
397+
#endif
398+
return BigInt.asUintN(bitSize, value);
399+
}
400+
maxRange = fromWireType(maxRange);
392401
}
393402

394403
registerType(primitiveType, {
395404
name,
396-
'fromWireType': (value) => value,
397-
'toWireType': function(destructors, value) {
398-
if (typeof value != "bigint" && typeof value != "number") {
399-
throw new TypeError(`Cannot convert "${embindRepr(value)}" to ${this.name}`);
400-
}
405+
'fromWireType': fromWireType,
406+
'toWireType': (destructors, value) => {
401407
if (typeof value == "number") {
402408
value = BigInt(value);
403409
}
404410
#if ASSERTIONS
405-
if (value < minRange || value > maxRange) {
406-
throw new TypeError(`Passing a number "${embindRepr(value)}" from JS side to C/C++ side to an argument of type "${name}", which is outside the valid range [${minRange}, ${maxRange}]!`);
411+
else if (typeof value != "bigint") {
412+
throw new TypeError(`Cannot convert "${embindRepr(value)}" to ${this.name}`);
407413
}
414+
assertIntegerRange(name, value, minRange, maxRange);
408415
#endif
409416
return value;
410417
},
@@ -1193,7 +1200,7 @@ var LibraryEmbind = {
11931200
$constNoSmartPtrRawPointerToWireType__docs: '/** @suppress {globalThis} */',
11941201
// If we know a pointer type is not going to have SmartPtr logic in it, we can
11951202
// special-case optimize it a bit (compare to genericPointerToWireType)
1196-
$constNoSmartPtrRawPointerToWireType__deps: ['$throwBindingError', '$upcastPointer'],
1203+
$constNoSmartPtrRawPointerToWireType__deps: ['$throwBindingError', '$upcastPointer', '$embindRepr'],
11971204
$constNoSmartPtrRawPointerToWireType: function(destructors, handle) {
11981205
if (handle === null) {
11991206
if (this.isReference) {
@@ -1216,7 +1223,7 @@ var LibraryEmbind = {
12161223
$nonConstNoSmartPtrRawPointerToWireType__docs: '/** @suppress {globalThis} */',
12171224
// An optimized version for non-const method accesses - there we must additionally restrict that
12181225
// the pointer is not a const-pointer.
1219-
$nonConstNoSmartPtrRawPointerToWireType__deps: ['$throwBindingError', '$upcastPointer'],
1226+
$nonConstNoSmartPtrRawPointerToWireType__deps: ['$throwBindingError', '$upcastPointer', '$embindRepr'],
12201227
$nonConstNoSmartPtrRawPointerToWireType: function(destructors, handle) {
12211228
if (handle === null) {
12221229
if (this.isReference) {

src/lib/libembind_gen.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,8 @@ var LibraryEmbind = {
367367
['unsigned long', ['bigint']],
368368
#endif
369369
#if WASM_BIGINT
370-
['int64_t', ['bigint']],
371-
['uint64_t', ['bigint']],
370+
['long long', ['bigint']],
371+
['unsigned long long', ['bigint']],
372372
#endif
373373
['void', ['void']],
374374
['std::string', [jsString, 'string']],

system/lib/embind/bind.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ EMSCRIPTEN_BINDINGS(builtin) {
142142
register_integer<unsigned long>("unsigned long");
143143
#endif
144144

145-
register_bigint<int64_t>("int64_t");
146-
register_bigint<uint64_t>("uint64_t");
145+
register_bigint<signed long long>("long long");
146+
register_bigint<unsigned long long>("unsigned long long");
147147

148148
register_float<float>("float");
149149
register_float<double>("double");

test/code_size/embind_hello_wasm.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.html": 552,
33
"a.html.gz": 380,
4-
"a.js": 9005,
5-
"a.js.gz": 3961,
6-
"a.wasm": 7332,
7-
"a.wasm.gz": 3369,
8-
"total": 16889,
9-
"total_gz": 7710
4+
"a.js": 8831,
5+
"a.js.gz": 3897,
6+
"a.wasm": 7344,
7+
"a.wasm.gz": 3368,
8+
"total": 16727,
9+
"total_gz": 7645
1010
}

test/code_size/embind_val_wasm.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"a.html": 552,
33
"a.html.gz": 380,
4-
"a.js": 6862,
5-
"a.js.gz": 2965,
6-
"a.wasm": 9133,
7-
"a.wasm.gz": 4710,
8-
"total": 16547,
9-
"total_gz": 8055
4+
"a.js": 6688,
5+
"a.js.gz": 2893,
6+
"a.wasm": 9137,
7+
"a.wasm.gz": 4700,
8+
"total": 16377,
9+
"total_gz": 7973
1010
}

test/embind/test_embind_long_long.cpp

Lines changed: 0 additions & 23 deletions
This file was deleted.

test/embind/test_i64_binding.cpp

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,24 @@
1313
using namespace emscripten;
1414
using namespace std;
1515

16-
#define assert_js(X) assert(run_js(X))
17-
1816
void test(string message)
1917
{
20-
cout << "test:\n" << message << "\n";
21-
}
22-
23-
24-
void execute_js(string js_code)
25-
{
26-
js_code.append(";");
27-
const char* js_code_pointer = js_code.c_str();
28-
EM_ASM_INT({
29-
var js_code = UTF8ToString($0);
30-
return eval(js_code);
31-
}, js_code_pointer);
18+
printf("test: %s\n", message.c_str());
3219
}
3320

34-
int run_js(string js_code)
35-
{
36-
js_code.append(";");
37-
const char* js_code_pointer = js_code.c_str();
38-
return EM_ASM_INT({
39-
var js_code = UTF8ToString($0);
40-
return eval(js_code);
41-
}, js_code_pointer);
21+
void assert_js_eq(string X, string Y) {
22+
string js_code;
23+
js_code += "const x = " + X + ";";
24+
js_code += "const y = " + Y + ";";
25+
js_code += "assert(x === y, `" + X + ": actual = ${typeof x} ${x}, expected = ${typeof y} ${y}`);";
26+
emscripten_run_script(js_code.c_str());
4227
}
4328

4429
EMSCRIPTEN_BINDINGS(tests) {
30+
emscripten::function("int64_min", &numeric_limits<int64_t>::min);
31+
emscripten::function("int64_max", &numeric_limits<int64_t>::max);
32+
emscripten::function("uint64_max", &numeric_limits<uint64_t>::max);
33+
4534
register_vector<int64_t>("Int64Vector");
4635
register_vector<uint64_t>("UInt64Vector");
4736
}
@@ -50,42 +39,44 @@ extern "C" void ensure_js_throws_with_assertions_enabled(const char* js_code, co
5039

5140
int main()
5241
{
53-
const int64_t max_int64_t = numeric_limits<int64_t>::max();
54-
const int64_t min_int64_t = numeric_limits<int64_t>::min();
55-
const uint64_t max_uint64_t = numeric_limits<uint64_t>::max();
42+
test("limits");
43+
44+
assert_js_eq("Module.int64_min()", to_string(numeric_limits<int64_t>::min()) + "n");
45+
assert_js_eq("Module.int64_max()", to_string(numeric_limits<int64_t>::max()) + "n");
46+
assert_js_eq("Module.uint64_max()", to_string(numeric_limits<uint64_t>::max()) + "n");
5647

5748
printf("start\n");
5849

5950
test("vector<int64_t>");
6051
val myval(std::vector<int64_t>{1, 2, 3, -4});
6152
val::global().set("v64", myval);
62-
assert_js("v64.get(0) === 1n");
63-
assert_js("v64.get(1) === 2n");
64-
assert_js("v64.get(2) === 3n");
65-
assert_js("v64.get(3) === -4n");
53+
assert_js_eq("v64.get(0)", "1n");
54+
assert_js_eq("v64.get(1)", "2n");
55+
assert_js_eq("v64.get(2)", "3n");
56+
assert_js_eq("v64.get(3)", "-4n");
6657

67-
execute_js("v64.push_back(1234n)");
68-
assert_js("v64.size() === 5");
69-
assert_js("v64.get(4) === 1234n");
58+
emscripten_run_script("v64.push_back(1234n)");
59+
assert_js_eq("v64.size()", "5");
60+
assert_js_eq("v64.get(4)", "1234n");
7061

7162
test("vector<int64_t> Cannot convert bigint that is too big");
7263
ensure_js_throws_with_assertions_enabled("v64.push_back(12345678901234567890123456n)", "TypeError");
7364

7465
test("vector<uint64_t>");
7566
val myval2(vector<uint64_t>{1, 2, 3, 4});
7667
val::global().set("vU64", myval2);
77-
assert_js("vU64.get(0) === 1n");
78-
assert_js("vU64.get(1) === 2n");
79-
assert_js("vU64.get(2) === 3n");
80-
assert_js("vU64.get(3) === 4n");
81-
82-
execute_js("vU64.push_back(1234n)");
83-
assert_js("vU64.size() === 5");
84-
assert_js("vU64.get(4) === 1234n");
85-
86-
execute_js("vU64.push_back(1234)");
87-
assert_js("vU64.size() === 6");
88-
assert_js("vU64.get(5) === 1234n");
68+
assert_js_eq("vU64.get(0)", "1n");
69+
assert_js_eq("vU64.get(1)", "2n");
70+
assert_js_eq("vU64.get(2)", "3n");
71+
assert_js_eq("vU64.get(3)", "4n");
72+
73+
emscripten_run_script("vU64.push_back(1234n)");
74+
assert_js_eq("vU64.size()", "5");
75+
assert_js_eq("vU64.get(4)", "1234n");
76+
77+
emscripten_run_script("vU64.push_back(1234)");
78+
assert_js_eq("vU64.size()", "6");
79+
assert_js_eq("vU64.get(5)", "1234n");
8980

9081
test("vector<uint64_t> Cannot convert bigint that is too big");
9182
ensure_js_throws_with_assertions_enabled("vU64.push_back(12345678901234567890123456n)", "TypeError");

test/embind/test_i64_binding.out

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1+
test: limits
12
start
2-
test:
3-
vector<int64_t>
4-
test:
5-
vector<int64_t> Cannot convert bigint that is too big
6-
test:
7-
vector<uint64_t>
8-
test:
9-
vector<uint64_t> Cannot convert bigint that is too big
10-
test:
11-
vector<uint64_t> Cannot convert bigint that is negative
3+
test: vector<int64_t>
4+
test: vector<int64_t> Cannot convert bigint that is too big
5+
test: vector<uint64_t>
6+
test: vector<uint64_t> Cannot convert bigint that is too big
7+
test: vector<uint64_t> Cannot convert bigint that is negative
128
end

test/test_other.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3396,14 +3396,6 @@ def test_embind_return_value_policy(self):
33963396

33973397
self.do_runf('embind/test_return_value_policy.cpp')
33983398

3399-
@parameterized({
3400-
'': [[]],
3401-
'asyncify': [['-sASYNCIFY=1']],
3402-
})
3403-
def test_embind_long_long(self, args):
3404-
self.do_runf('embind/test_embind_long_long.cpp', '1000000000000n\n-1000000000000n',
3405-
emcc_args=['-lembind', '-sWASM_BIGINT'] + args)
3406-
34073399
@requires_node_canary
34083400
def test_embind_resource_management(self):
34093401
self.node_args.append('--js-explicit-resource-management')
@@ -3582,7 +3574,7 @@ def test_embind_tsgen_bigint(self):
35823574
args = [EMXX, test_file('other/embind_tsgen_bigint.cpp'), '-lembind', '--emit-tsd', 'embind_tsgen_bigint.d.ts']
35833575
# Check that TypeScript generation fails when code contains bigints but their support is not enabled
35843576
stderr = self.expect_fail(args + ['-sWASM_BIGINT=0'])
3585-
self.assertContained("Missing primitive type to TS type for 'int64_t", stderr)
3577+
self.assertContained("Missing primitive type to TS type for 'long long", stderr)
35863578
# Check that TypeScript generation works when bigint support is enabled
35873579
self.run_process(args)
35883580
self.assertFileContents(test_file('other/embind_tsgen_bigint.d.ts'), read_file('embind_tsgen_bigint.d.ts'))

0 commit comments

Comments
 (0)