Skip to content

Wasm/JS BigInt support #10860

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions emcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2019,6 +2019,9 @@ def check_human_readable_list(items):
# requires JS legalization
shared.Settings.LEGALIZE_JS_FFI = 0

if shared.Settings.WASM_BIGINT:
shared.Settings.LEGALIZE_JS_FFI = 0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is legalization only dealing with the i64 issue?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used to legalize f32s to doubles for JS too, but it turned out that was not necessary. So it is just i64s now, yes.


if shared.Settings.WASM_BACKEND:
if shared.Settings.SIMD:
newargs.append('-msimd128')
Expand Down
2 changes: 2 additions & 0 deletions emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -2327,6 +2327,8 @@ def finalize_wasm(temp_files, infile, outfile, memfile, DEBUG):
# (which matches what llvm+lld has given us)
if shared.Settings.DEBUG_LEVEL >= 2 or shared.Settings.PROFILING_FUNCS or shared.Settings.EMIT_SYMBOL_MAP or shared.Settings.ASYNCIFY_WHITELIST or shared.Settings.ASYNCIFY_BLACKLIST:
args.append('-g')
if shared.Settings.WASM_BIGINT:
args.append('--bigint')
if shared.Settings.LEGALIZE_JS_FFI != 1:
args.append('--no-legalize-javascript-ffi')
if not shared.Settings.MEM_INIT_IN_WASM:
Expand Down
4 changes: 4 additions & 0 deletions src/closure-externs/closure-externs.js
Original file line number Diff line number Diff line change
Expand Up @@ -994,3 +994,7 @@ var threadInfoStruct;
var selfThreadId;
/** @suppress {duplicate} */
var noExitRuntime;

// No BigInt in closure yet
// https://github.com/google/closure-compiler/issues/3167
var BigInt;
7 changes: 5 additions & 2 deletions src/library_syscall.js
Original file line number Diff line number Diff line change
Expand Up @@ -1390,7 +1390,9 @@ var SyscallsLibrary = {
FS.utime(path, atime, mtime);
return 0;
},
__sys_fallocate: function(fd, mode, off_low, off_high, len_low, len_high) {
__sys_fallocate: function(fd, mode, {{{ defineI64Param('off') }}}, {{{ defineI64Param('len') }}}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't tell whether this is gross or not.
Either way I like it :)

{{{ receiveI64ParamAsI32s('off') }}}
{{{ receiveI64ParamAsI32s('len') }}}
var stream = SYSCALLS.getStreamFromFD(fd)
var offset = SYSCALLS.get64(off_low, off_high);
var len = SYSCALLS.get64(len_low, len_high);
Expand Down Expand Up @@ -1522,7 +1524,8 @@ var SyscallsLibrary = {
return 0;
},
fd_seek__sig: 'iiiiii',
fd_seek: function(fd, offset_low, offset_high, whence, newOffset) {
fd_seek: function(fd, {{{ defineI64Param('offset') }}}, whence, newOffset) {
{{{ receiveI64ParamAsI32s('offset') }}}
var stream = SYSCALLS.getStreamFromFD(fd);
var HIGH_OFFSET = 0x100000000; // 2^32
// use an unsigned operator on low and shift high by 32-bits
Expand Down
3 changes: 2 additions & 1 deletion src/library_wasi.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ var WasiLibrary = {
// either wait for BigInt support or to legalize on the client.
clock_time_get__sig: 'iiiii',
clock_time_get__deps: ['emscripten_get_now', 'emscripten_get_now_is_monotonic', '__setErrNo'],
clock_time_get: function(clk_id, precision_l, precision_h, ptime) {
clock_time_get: function(clk_id, {{{ defineI64Param('precision') }}}, ptime) {
{{{ receiveI64ParamAsI32s('precision') }}}
var now;
if (clk_id === {{{ cDefine('__WASI_CLOCKID_REALTIME') }}}) {
now = Date.now();
Expand Down
34 changes: 23 additions & 11 deletions src/library_webgpu.js
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,8 @@ var LibraryWebGPU = {

// wgpuFence

wgpuFenceOnCompletion: function(fenceId, completionValue_l, completionValue_h, callback, userdata) {
wgpuFenceOnCompletion: function(fenceId, {{{ defineI64Param('completionValue') }}}, callback, userdata) {
{{{ receiveI64ParamAsI32s('completionValue') }}}
var fence = WebGPU.mgrFence.get(fenceId);
var completionValue = {{{ gpu.makeU64ToNumber('completionValue_l', 'completionValue_h') }}};

Expand Down Expand Up @@ -990,7 +991,8 @@ var LibraryWebGPU = {
return WebGPU.mgrFence.create(queue.createFence(desc));
},

wgpuQueueSignal: function(queueId, fenceId, signalValue_l, signalValue_h) {
wgpuQueueSignal: function(queueId, fenceId, {{{ defineI64Param('signalValue') }}}) {
{{{ receiveI64ParamAsI32s('signalValue') }}}
var queue = WebGPU.mgrQueue.get(queueId);
var fence = WebGPU.mgrFence.get(fenceId);
var signalValue = {{{ gpu.makeU64ToNumber('signalValue_l', 'signalValue_h') }}};
Expand Down Expand Up @@ -1135,7 +1137,10 @@ var LibraryWebGPU = {
return WebGPU.mgrRenderPassEncoder.create(commandEncoder["beginRenderPass"](desc));
},

wgpuCommandEncoderCopyBufferToBuffer: function(encoderId, srcId, srcOffset_l, srcOffset_h, dstId, dstOffset_l, dstOffset_h, size_l, size_h) {
wgpuCommandEncoderCopyBufferToBuffer: function(encoderId, srcId, {{{ defineI64Param('srcOffset') }}}, dstId, {{{ defineI64Param('dstOffset') }}}, {{{ defineI64Param('size') }}}) {
{{{ receiveI64ParamAsI32s('srcOffset') }}}
{{{ receiveI64ParamAsI32s('dstOffset') }}}
{{{ receiveI64ParamAsI32s('size') }}}
var commandEncoder = WebGPU.mgrCommandEncoder.get(encoderId);
var src = WebGPU.mgrBuffer.get(srcId);
var dst = WebGPU.mgrBuffer.get(dstId);
Expand Down Expand Up @@ -1168,7 +1173,9 @@ var LibraryWebGPU = {

// wgpuBuffer

wgpuBufferSetSubData: function(bufferId, start_l, start_h, count_l, count_h, data) {
wgpuBufferSetSubData: function(bufferId, {{{ defineI64Param('start') }}}, {{{ defineI64Param('count') }}}, data) {
{{{ receiveI64ParamAsI32s('start') }}}
{{{ receiveI64ParamAsI32s('count') }}}
var buffer = WebGPU.mgrBuffer.get(bufferId);
var start = {{{ gpu.makeU64ToNumber('start_l', 'start_h') }}};
var count = {{{ gpu.makeU64ToNumber('count_l', 'count_h') }}};
Expand All @@ -1187,7 +1194,7 @@ var LibraryWebGPU = {
var dataLength_h = (mapped.byteLength / 0x100000000) | 0;
var dataLength_l = mapped.byteLength | 0;
// WGPUBufferMapAsyncStatus status, const void* data, uint64_t dataLength, void* userdata
dynCall('viiji', callback, [WEBGPU_BUFFER_MAP_ASYNC_STATUS_SUCCESS, data, dataLength_l, dataLength_h, userdata]);
dynCall('viiji', callback, [WEBGPU_BUFFER_MAP_ASYNC_STATUS_SUCCESS, data, {{{ sendI64Argument('dataLength_l', 'dataLength_h') }}}, userdata]);
}, function() {
// TODO(kainino0x): Figure out how to pick other error status values.
var WEBGPU_BUFFER_MAP_ASYNC_STATUS_ERROR = 1;
Expand All @@ -1208,7 +1215,7 @@ var LibraryWebGPU = {
var dataLength_h = (mapped.byteLength / 0x100000000) | 0;
var dataLength_l = mapped.byteLength | 0;
// WGPUBufferMapAsyncStatus status, void* data, uint64_t dataLength, void* userdata
dynCall('viiji', callback, [WEBGPU_BUFFER_MAP_ASYNC_STATUS_SUCCESS, data, dataLength_l, dataLength_h, userdata]);
dynCall('viiji', callback, [WEBGPU_BUFFER_MAP_ASYNC_STATUS_SUCCESS, data, {{{ sendI64Argument('dataLength_l', 'dataLength_h') }}}, userdata]);
}, function() {
// TODO(kainino0x): Figure out how to pick other error status values.
dynCall('viiji', callback, [WEBGPU_BUFFER_MAP_ASYNC_STATUS_ERROR, 0, 0, 0, userdata]);
Expand Down Expand Up @@ -1271,7 +1278,8 @@ var LibraryWebGPU = {
var pass = WebGPU.mgrComputePassEncoder.get(passId);
pass["dispatch"](x, y, z);
},
wgpuComputePassEncoderDispatchIndirect: function(passId, indirectBufferId, indirectOffset_l, indirectOffset_h) {
wgpuComputePassEncoderDispatchIndirect: function(passId, indirectBufferId, {{{ defineI64Param('indirectOffset') }}}) {
{{{ receiveI64ParamAsI32s('indirectOffset') }}}
var indirectBuffer = WebGPU.mgrBuffer.get(indirectBufferId);
var indirectOffset = {{{ gpu.makeU64ToNumber('indirectOffset_l', 'indirectOffset_h') }}};
var pass = WebGPU.mgrComputePassEncoder.get(passId);
Expand Down Expand Up @@ -1336,13 +1344,15 @@ var LibraryWebGPU = {
var pass = WebGPU.mgrRenderPassEncoder.get(passId);
pass["drawIndexed"](indexCount, instanceCount, firstIndex, baseVertex, firstInstance);
},
wgpuRenderPassEncoderDrawIndirect: function(passId, indirectBufferId, indirectOffset_l, indirectOffset_h) {
wgpuRenderPassEncoderDrawIndirect: function(passId, indirectBufferId, {{{ defineI64Param('indirectOffset') }}}) {
{{{ receiveI64ParamAsI32s('indirectOffset') }}}
var indirectBuffer = WebGPU.mgrBuffer.get(indirectBufferId);
var indirectOffset = {{{ gpu.makeU64ToNumber('indirectOffset_l', 'indirectOffset_h') }}};
var pass = WebGPU.mgrRenderPassEncoder.get(passId);
pass["drawIndirect"](indirectBuffer, indirectOffset);
},
wgpuRenderPassEncoderDrawIndexedIndirect: function(passId, indirectBufferId, indirectOffset_l, indirectOffset_h) {
wgpuRenderPassEncoderDrawIndexedIndirect: function(passId, indirectBufferId, {{{ defineI64Param('indirectOffset') }}}) {
{{{ receiveI64ParamAsI32s('indirectOffset') }}}
var indirectBuffer = WebGPU.mgrBuffer.get(indirectBufferId);
var indirectOffset = {{{ gpu.makeU64ToNumber('indirectOffset_l', 'indirectOffset_h') }}};
var pass = WebGPU.mgrRenderPassEncoder.get(passId);
Expand Down Expand Up @@ -1379,7 +1389,8 @@ var LibraryWebGPU = {
pass["setBindGroup"](groupIndex, group, offsets);
}
},
wgpuRenderBundleEncoderSetIndexBuffer: function(bundleId, bufferId, offset_l, offset_h) {
wgpuRenderBundleEncoderSetIndexBuffer: function(bundleId, bufferId, {{{ defineI64Param('offset') }}}) {
{{{ receiveI64ParamAsI32s('offset') }}}
var offset = {{{ gpu.makeU64ToNumber('offset_l', 'offset_h') }}};
var pass = WebGPU.mgrRenderBundleEncoder.get(bundleId);
var buffer = WebGPU.mgrBuffer.get(bufferId);
Expand All @@ -1390,7 +1401,8 @@ var LibraryWebGPU = {
var pipeline = WebGPU.mgrRenderPipeline.get(pipelineId);
pass["setPipeline"](pipeline);
},
wgpuRenderBundleEncoderSetVertexBuffer: function(bundleId, slot, bufferId, offset_l, offset_h) {
wgpuRenderBundleEncoderSetVertexBuffer: function(bundleId, slot, bufferId, {{{ defineI64Param('offset') }}}) {
{{{ receiveI64ParamAsI32s('offset') }}}
var offset = {{{ gpu.makeU64ToNumber('offset_l', 'offset_h') }}};
var pass = WebGPU.mgrRenderBundleEncoder.get(bundleId);
pass["setVertexBuffer"](slot, WebGPU.mgrBuffer.get(bufferId), offset);
Expand Down
31 changes: 31 additions & 0 deletions src/parseTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -1677,3 +1677,34 @@ function makeAsmImportsAccessInPthread(variable) {
function hasExportedFunction(func) {
return Object.keys(EXPORTED_FUNCTIONS).indexOf(func) != -1;
}

// JS API I64 param handling: if we have BigInt support, the ABI is simple,
// it is a BigInt. Otherwise, we legalize into pairs of i32s.
function defineI64Param(name) {
if (WASM_BIGINT) {
return name + '_bigint';
} else {
return name + '_low, ' + name + '_high';
}
}

function receiveI64ParamAsI32s(name) {
if (WASM_BIGINT) {
// TODO: use Xn notation when JS parsers support it (as of April 6 2020,
// * closure compiler is missing support
// https://github.com/google/closure-compiler/issues/3167
// * acorn needs to be upgraded, and to set ecmascript version >= 11
// * terser needs to be upgraded
return 'var ' + name + '_low = Number(' + name + '_bigint & BigInt(0xffffffff)) | 0, ' + name + '_high = Number(' + name + '_bigint >> BigInt(32)) | 0;';
} else {
return '';
}
}

function sendI64Argument(low, high) {
if (WASM_BIGINT) {
return 'BigInt(low) | (BigInt(high) << BigInt(32))';
} else {
return low + ', ' + high;
}
}
5 changes: 5 additions & 0 deletions src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,11 @@ var BINARYEN_EXTRA_PASSES = "";
// (This option was formerly called BINARYEN_ASYNC_COMPILATION)
var WASM_ASYNC_COMPILATION = 1;

// WebAssembly integration with JavaScript BigInt. When enabled we don't need
// to legalize i64s into pairs of i32s, as the wasm VM will use a BigInt where
// an i64 is used.
var WASM_BIGINT = 0;

// WebAssembly defines a "producers section" which compilers and tools can
// annotate themselves in. Emscripten does not emit this by default, as it
// increases code size, and some users may not want information about their tools
Expand Down
10 changes: 9 additions & 1 deletion src/support.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,9 +444,13 @@ function loadWebAssemblyModule(binary, flags) {
var moduleLocal = {};

var resolveSymbol = function(sym, type, legalized) {
#if WASM_BIGINT
assert(!legalized);
#else
if (legalized) {
sym = 'orig$' + sym;
}
#endif

var resolved = Module["asm"][sym];
if (!resolved) {
Expand All @@ -460,7 +464,7 @@ function loadWebAssemblyModule(binary, flags) {
#if ASSERTIONS
assert(resolved, 'missing linked ' + type + ' `' + sym + '`. perhaps a side module was not linked in? if this global was expected to arrive from a system library, try to build the MAIN_MODULE with EMCC_FORCE_STDLIBS=1 in the environment');
#endif
}
}
return resolved;
}

Expand Down Expand Up @@ -510,7 +514,11 @@ function loadWebAssemblyModule(binary, flags) {
assert(parts.length == 3)
var name = parts[1];
var sig = parts[2];
#if WASM_BIGINT
var legalized = false;
#else
var legalized = sig.indexOf('j') >= 0; // check for i64s
#endif
var fp = 0;
return obj[prop] = function() {
if (!fp) {
Expand Down
29 changes: 29 additions & 0 deletions tests/core/test_i64_invoke_bigint.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

#include <emscripten.h>
#include <stdio.h>
#include <stdint.h>

int64_t big = 0x12345678aabbccddL;

__attribute__((noinline))
int64_t foobar(int64_t x, int y) {
x += EM_ASM_INT({
return 0; // prevents llvm from seeing the final value
});
if (x == 1337) {
throw 1; // looks like we might throw
}
return x + y; // use the int parameter too, to show they are all handled
}

int main() {
int64_t x;
try {
puts("try");
x = foobar(big, 1);
} catch(int) {
puts("caught");
}
printf("ok: 0x%llx.\n", x);
}

2 changes: 2 additions & 0 deletions tests/core/test_i64_invoke_bigint.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
try
ok: 0x12345678aabbccde.
17 changes: 13 additions & 4 deletions tests/return64bit/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@
* found in the LICENSE file.
*/

// This is just a trivial test function, the key bit of interest is that it returns a 64 bit long.
long long test_return64() {
long long x = ((long long)1234 << 32) + 5678;
return x;
#include <emscripten.h>
#include <stdio.h>
#include <stdint.h>

int64_t test_return64(int64_t input) {
int64_t x = ((int64_t)1234 << 32) + 5678;
printf("input = 0x%llx\n", input);
return x;
}

EMSCRIPTEN_KEEPALIVE
void* get_func_ptr() {
return &test_return64;
}
15 changes: 9 additions & 6 deletions tests/return64bit/testbind.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ var Module = {
};

Module['runtest'] = function() {
var low = _test_return64();
var high = getTempRet0();
var low = _test_return64(0x11223344, 0xaabbccdd);
var high = getTempRet0();
console.log("low = " + low);
console.log("high = " + high);

console.log("low = " + low);
console.log("high = " + high);
var ptr = _get_func_ptr();
low = dynCall_jj(ptr, 0x12345678, 0xabcdef19);
high = getTempRet0();
console.log("low = " + low);
console.log("high = " + high);
};


25 changes: 25 additions & 0 deletions tests/return64bit/testbind_bigint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// This code represents a simple native JavaScript binding to a test C function
// that returns a 64 bit long. Notice that the least significant 32 bits are
// returned in the normal return value, but the most significant 32 bits are
// returned via the accessor method Runtime.getTempRet0()

var Module = {
'noExitRuntime' : true
};

Module['runtest'] = function() {
// Use eval to create BigInt, as no support for Xn notation yet in JS
// optimizer.
var bigint = _test_return64(eval('0xaabbccdd11223344n'));
var low = Number(bigint & BigInt(0xffffffff));
var high = Number(bigint >> BigInt(32));
console.log("low = " + low);
console.log("high = " + high);

var ptr = _get_func_ptr();
bigint = dynCall_jj(ptr, eval('0xabcdef1912345678n'));
low = Number(bigint & BigInt(0xffffffff));
high = Number(bigint >> BigInt(32));
console.log("low = " + low);
console.log("high = " + high);
};
Loading