Skip to content

Commit

Permalink
[jspi] Switch to new JSPI API. (#21815)
Browse files Browse the repository at this point in the history
Differences with the new API:
 - The JS engine now takes care of the suspender, so we don't need
  to modify the wasm file with binaryen.
 - Imports and exports and are now marked as async with
  WebAssembly.Suspending and WebAssembly.promising wrappers.
  • Loading branch information
brendandahl committed May 22, 2024
1 parent 228af1a commit 4d22ffe
Show file tree
Hide file tree
Showing 8 changed files with 32 additions and 91 deletions.
3 changes: 3 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ See docs/process.md for more on how version tagging works.

3.1.61 (in development)
-----------------------
- The JSPI feature now uses the updated browser API for JSPI (available in
Chrome v126+). To support older versions of Chrome use Emscripten version
3.1.60 or earlier.

3.1.60 - 05/20/24
-----------------
Expand Down
25 changes: 11 additions & 14 deletions src/closure-externs/closure-externs.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ WebAssembly.Memory.prototype.buffer;
* @type {number}
*/
WebAssembly.Table.prototype.length;
/**
* @param {!Function} func
* @returns {Function}
*/
WebAssembly.promising = function(func) {};
/**
* @constructor
* @param {!Function} func
*/
WebAssembly.Suspending = function(func) {};

/**
* @record
Expand All @@ -125,26 +135,13 @@ FunctionType.prototype.parameters;
* @type {Array<string>}
*/
FunctionType.prototype.results;
/**
* @record
*/
function FunctionUsage() {}
/**
* @type {string|undefined}
*/
FunctionUsage.prototype.promising;
/**
* @type {string|undefined}
*/
FunctionUsage.prototype.suspending;

/**
* @constructor
* @param {!FunctionType} type
* @param {!Function} func
* @param {FunctionUsage=} usage
*/
WebAssembly.Function = function(type, func, usage) {};
WebAssembly.Function = function(type, func) {};
/**
* @param {Function} func
* @return {FunctionType}
Expand Down
6 changes: 6 additions & 0 deletions src/embind/embind.js
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,12 @@ var LibraryEmbind = {
assert(!isAsync, 'Async bindings are only supported with JSPI.');
#endif

#if ASYNCIFY == 2
if (isAsync) {
cppInvokerFunc = Asyncify.makeAsyncFunction(cppInvokerFunc);
}
#endif

var isClassMethodFunc = (argTypes[1] !== null && classType !== null);

// Free functions with signature "void function()" do not need an invoker that marshalls between wire types.
Expand Down
8 changes: 2 additions & 6 deletions src/jsifier.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -629,12 +629,8 @@ function(${args}) {
if ((EXPORT_ALL || EXPORTED_FUNCTIONS.has(mangled)) && !isStub) {
contentText += `\nModule['${mangled}'] = ${mangled};`;
}
// Relocatable code needs signatures to create proper wrappers. Stack
// switching needs signatures so we can create a proper
// WebAssembly.Function with the signature for the Promise API.
// TODO: For asyncify we could only add the signatures we actually need,
// of async imports/exports.
if (sig && (RELOCATABLE || ASYNCIFY == 2)) {
// Relocatable code needs signatures to create proper wrappers.
if (sig && RELOCATABLE) {
if (!WASM_BIGINT) {
sig = sig[0].replace('j', 'i') + sig.slice(1).replace(/j/g, 'ii');
}
Expand Down
30 changes: 3 additions & 27 deletions src/library_async.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ addToLibrary({
dbg('asyncify instrumenting imports');
#endif
#if ASSERTIONS && ASYNCIFY == 2
assert('Suspender' in WebAssembly, 'JSPI not supported by current environment. Perhaps it needs to be enabled via flags?');
assert('Suspending' in WebAssembly, 'JSPI not supported by current environment. Perhaps it needs to be enabled via flags?');
#endif
var importPattern = {{{ new RegExp(`^(${ASYNCIFY_IMPORTS_EXCEPT_JS_LIBS.map(x => x.split('.')[1]).join('|').replace(/\*/g, '.*')})$`) }}};

Expand All @@ -52,21 +52,10 @@ addToLibrary({
#if ASYNCIFY == 2
// Wrap async imports with a suspending WebAssembly function.
if (isAsyncifyImport) {
#if ASSERTIONS
assert(original.sig, `Missing __sig for ${x}`);
#endif
let type = sigToWasmTypes(original.sig);
#if ASYNCIFY_DEBUG
dbg('asyncify: suspendOnReturnedPromise for', x, original);
#endif
// Add space for the suspender promise that will be used in the
// Wasm wrapper function.
type.parameters.unshift('externref');
imports[x] = original = new WebAssembly.Function(
type,
original,
{ suspending: 'first' }
);
imports[x] = original = new WebAssembly.Suspending(original);
}
#endif
#if ASSERTIONS && ASYNCIFY != 2 // We cannot apply assertions with stack switching, as the imports must not be modified from suspender.suspendOnReturnedPromise TODO find a way
Expand Down Expand Up @@ -454,20 +443,7 @@ addToLibrary({
#if ASYNCIFY_DEBUG
dbg('asyncify: returnPromiseOnSuspend for', original);
#endif
// TODO: remove `WebAssembly.Function.type` call when the new API is ready on all the testers.
var type = original.type ? original.type() : WebAssembly.Function.type(original);
var parameters = type.parameters;
var results = type.results;
#if ASSERTIONS
assert(results.length !== 0, 'There must be a return result')
assert(parameters[0] === 'externref', 'First param must be externref.');
#endif
// Remove the extern ref.
parameters.shift();
return new WebAssembly.Function(
{ parameters , results: ['externref'] },
original,
{ promising : 'first' });
return WebAssembly.promising(original);
},
#endif
},
Expand Down
40 changes: 7 additions & 33 deletions system/include/emscripten/bind.h
Original file line number Diff line number Diff line change
Expand Up @@ -454,25 +454,6 @@ struct Invoker<ReturnPolicy, void, Args...> {
}
};

namespace async {

template<typename F, F f> struct Wrapper;
template<typename ReturnType, typename... Args, ReturnType(*f)(Args...)>
struct Wrapper<ReturnType(*)(Args...), f> {
EMSCRIPTEN_KEEPALIVE static ReturnType invoke(Args... args) {
return f(args...);
}
};

} // end namespace async

template<typename T, typename... Policies>
using maybe_wrap_async = typename std::conditional<
isAsync<Policies...>::value,
async::Wrapper<decltype(&T::invoke), &T::invoke>,
T
>::type;

template<typename ReturnPolicy, typename FunctorType, typename ReturnType, typename... Args>
struct FunctorInvoker {
static typename internal::BindingType<ReturnType>::WireType invoke(
Expand Down Expand Up @@ -601,8 +582,7 @@ void function(const char* name, ReturnType (*fn)(Args...), Policies...) {
using namespace internal;
typename WithPolicies<Policies...>::template ArgTypeList<ReturnType, Args...> args;
using ReturnPolicy = GetReturnValuePolicy<ReturnType, Policies...>::tag;
using OriginalInvoker = Invoker<ReturnPolicy, ReturnType, Args...>;
auto invoke = &maybe_wrap_async<OriginalInvoker, Policies...>::invoke;
auto invoke = Invoker<ReturnPolicy, ReturnType, Args...>::invoke;
_embind_register_function(
name,
args.getCount(),
Expand Down Expand Up @@ -1469,8 +1449,7 @@ struct RegisterClassMethod<ReturnType (ClassType::*)(Args...)> {
static void invoke(const char* methodName,
ReturnType (ClassType::*memberFunction)(Args...)) {
using ReturnPolicy = GetReturnValuePolicy<ReturnType, Policies...>::tag;
using OriginalInvoker = MethodInvoker<ReturnPolicy, decltype(memberFunction), ReturnType, ClassType*, Args...>;
auto invoke = &maybe_wrap_async<OriginalInvoker, Policies...>::invoke;
auto invoke = MethodInvoker<ReturnPolicy, decltype(memberFunction), ReturnType, ClassType*, Args...>::invoke;

typename WithPolicies<Policies...>::template ArgTypeList<ReturnType, AllowedRawPointer<ClassType>, Args...> args;
_embind_register_class_function(
Expand Down Expand Up @@ -1499,8 +1478,7 @@ struct RegisterClassMethod<ReturnType (ClassType::*)(Args...) const> {
static void invoke(const char* methodName,
ReturnType (ClassType::*memberFunction)(Args...) const) {
using ReturnPolicy = GetReturnValuePolicy<ReturnType, Policies...>::tag;
using OriginalInvoker = MethodInvoker<ReturnPolicy, decltype(memberFunction), ReturnType, const ClassType*, Args...>;
auto invoke = &maybe_wrap_async<OriginalInvoker, Policies...>::invoke;
auto invoke = MethodInvoker<ReturnPolicy, decltype(memberFunction), ReturnType, const ClassType*, Args...>::invoke;

typename WithPolicies<Policies...>::template ArgTypeList<ReturnType, AllowedRawPointer<const ClassType>, Args...> args;
_embind_register_class_function(
Expand Down Expand Up @@ -1530,8 +1508,7 @@ struct RegisterClassMethod<ReturnType (*)(ThisType, Args...)> {
ReturnType (*function)(ThisType, Args...)) {
typename WithPolicies<Policies...>::template ArgTypeList<ReturnType, ThisType, Args...> args;
using ReturnPolicy = GetReturnValuePolicy<ReturnType, Policies...>::tag;
using OriginalInvoker = FunctionInvoker<ReturnPolicy, decltype(function), ReturnType, ThisType, Args...>;
auto invoke = &maybe_wrap_async<OriginalInvoker, Policies...>::invoke;
auto invoke = FunctionInvoker<ReturnPolicy, decltype(function), ReturnType, ThisType, Args...>::invoke;
_embind_register_class_function(
TypeID<ClassType>::get(),
methodName,
Expand Down Expand Up @@ -1559,8 +1536,7 @@ struct RegisterClassMethod<std::function<ReturnType (ThisType, Args...)>> {
std::function<ReturnType (ThisType, Args...)> function) {
typename WithPolicies<Policies...>::template ArgTypeList<ReturnType, ThisType, Args...> args;
using ReturnPolicy = GetReturnValuePolicy<ReturnType, Policies...>::tag;
using OriginalInvoker = FunctorInvoker<ReturnPolicy, decltype(function), ReturnType, ThisType, Args...>;
auto invoke = &maybe_wrap_async<OriginalInvoker, Policies...>::invoke;
auto invoke = FunctorInvoker<ReturnPolicy, decltype(function), ReturnType, ThisType, Args...>::invoke;
_embind_register_class_function(
TypeID<ClassType>::get(),
methodName,
Expand All @@ -1582,8 +1558,7 @@ struct RegisterClassMethod<ReturnType (ThisType, Args...)> {
Callable& callable) {
typename WithPolicies<Policies...>::template ArgTypeList<ReturnType, ThisType, Args...> args;
using ReturnPolicy = GetReturnValuePolicy<ReturnType, Policies...>::tag;
using OriginalInvoker = FunctorInvoker<ReturnPolicy, decltype(callable), ReturnType, ThisType, Args...>;
auto invoke = &maybe_wrap_async<OriginalInvoker, Policies...>::invoke;
auto invoke = FunctorInvoker<ReturnPolicy, decltype(callable), ReturnType, ThisType, Args...>::invoke;
_embind_register_class_function(
TypeID<ClassType>::get(),
methodName,
Expand Down Expand Up @@ -1866,8 +1841,7 @@ class class_ {

typename WithPolicies<Policies...>::template ArgTypeList<ReturnType, Args...> args;
using ReturnPolicy = GetReturnValuePolicy<ReturnType, Policies...>::tag;
using OriginalInvoker = internal::Invoker<ReturnPolicy, ReturnType, Args...>;
auto invoke = &maybe_wrap_async<OriginalInvoker, Policies...>::invoke;
auto invoke = internal::Invoker<ReturnPolicy, ReturnType, Args...>::invoke;
_embind_register_class_class_function(
TypeID<ClassType>::get(),
methodName,
Expand Down
1 change: 0 additions & 1 deletion test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -4942,7 +4942,6 @@ def test_valid_abspath_2(self):
else:
abs_include_path = '/nowhere/at/all'
cmd = [EMCC, test_file('hello_world.c'), '--valid-abspath', abs_include_path, '-I%s' % abs_include_path]
print(' '.join(cmd))
self.run_process(cmd)
self.assertContained('hello, world!', self.run_js('a.out.js'))

Expand Down
10 changes: 0 additions & 10 deletions tools/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,6 @@
DEFAULT_ASYNCIFY_EXPORTS = [
'main',
'__main_argc_argv',
# Embind's async template wrapper functions. These functions are usually in
# the function pointer table and not called from exports, but we need to name
# them so the JSPI pass can find and convert them.
'_ZN10emscripten8internal5async*'
]

VALID_ENVIRONMENTS = ('web', 'webview', 'worker', 'node', 'shell')
Expand Down Expand Up @@ -395,12 +391,6 @@ def check_human_readable_list(items):
if settings.ASYNCIFY_ONLY:
check_human_readable_list(settings.ASYNCIFY_ONLY)
passes += ['--pass-arg=asyncify-onlylist@%s' % ','.join(settings.ASYNCIFY_ONLY)]
elif settings.ASYNCIFY == 2:
passes += ['--jspi']
passes += ['--pass-arg=jspi-imports@%s' % ','.join(settings.ASYNCIFY_IMPORTS)]
passes += ['--pass-arg=jspi-exports@%s' % ','.join(settings.ASYNCIFY_EXPORTS)]
if settings.SPLIT_MODULE:
passes += ['--pass-arg=jspi-split-module']

if settings.MEMORY64 == 2:
passes += ['--memory64-lowering']
Expand Down

0 comments on commit 4d22ffe

Please sign in to comment.