JavaScript Promise Integration (JSPI) API 允许编写的 WebAssembly 应用程序假设对外部功能的访问是同步的,以便在许多所需功能是异步的环境中顺利运行。
本说明概述了 JSPI API 的核心功能是什么、如何访问它、如何为其开发软件并提供了一些尝试示例。
JSPI 是一种在同步应用程序和异步 Web API 之间架起桥梁的 API。它通过在发出同步 API 调用时暂停应用程序并在异步 I/O 操作完成时恢复它来实现。至关重要的是,它只对应用程序本身进行了很少的更改。
Web 上的许多现代 API 本质上都是异步的。异步 API 通过将提供的功能分成两个独立的部分来运行:操作的启动及其解决;后者在第一个之后的某个时间出现。最重要的是,应用程序在启动操作后继续执行;然后在操作完成时收到通知。
例如,使用 fetch API 允许 Web 应用程序访问与 URL 关联的内容;但是,fetch 函数并不直接返回 fetch 的结果;相反,它返回一个承诺。通过将回调附加到该 Promise 来重新建立获取响应和原始请求之间的连接。回调函数可以检查响应并收集数据(当然如果有的话)。
正如经常记录的那样,直接使用 Promise 值非常困难。在 WebAssembly 应用程序的情况下,这个问题更加严重,因为它们不能直接操作 Promises。
JavaScript 的异步函数表示法在核心 API 之上提供了一层,显着减轻了 JavaScript 应用程序的负担。
另一方面,典型的 C/C++(和许多其他语言)应用程序最初通常是针对同步API 编写的。在这样的 API 中,应用程序将停止执行,直到操作完成。这种阻塞应用程序通常比异步感知的应用程序更容易编写。
但是,不允许阻塞浏览器的主线程;许多环境不支持同步编程。结果是应用程序程序员对简单易用的 API 的期望与需要使用异步代码构建 I/O 的更广泛的生态系统之间的不匹配。对于移植成本高昂的现有遗留应用程序来说,这尤其是一个问题。
JSPI 的工作原理是拦截从异步 API 调用返回的 Promise,暂停 WebAssembly 应用程序的主要逻辑,并从用于进入 WebAssembly 的导出返回 Promise。当异步 API 完成时,WebAssembly 应用程序将恢复,以便它可以处理 API 调用的结果。
这是通过在 WebAssembly 模块实例化期间包装导入和导出来实现的。函数包装器将挂起行为添加到正常的异步导入并将挂起路由到 Promise 回调。
没有必要包装 WebAssembly 模块的所有导出和导入。一些执行路径不涉及调用异步 API 的导出最好不要包装。同样,并非所有 WebAssembly 模块的导入都是针对异步 API 函数的;这些进口也不应该包装。
当然,有大量的内部机制允许这种情况发生;[1]但是 JavaScript 语言和 WebAssembly 本身都没有被 JSPI 改变。它的操作仅限于 JavaScript 和 WebAssembly 之间的边界。
从 Web 应用程序开发人员的角度来看,结果是代码主体以类似于用 JavaScript 编写的其他异步函数工作的方式参与异步函数和 Promises 的 JavaScript 世界。从 WebAssembly 开发人员的角度来看,这使他们能够使用同步 API 制作应用程序,同时参与 Web 的异步生态系统。
因为挂起和恢复 WebAssembly 模块时使用的机制本质上是恒定时间,所以我们预计使用 JSPI 的成本不会很高——尤其是与其他基于转换的方法相比。
将异步 API 调用返回的 Promise 传播到返回 Promise 的 WebAssembly 模块需要做大量工作。类似地,当一个 Promise 被 resolve 时,WebAssembly 应用程序可以以恒定时间开销立即恢复。
然而,与浏览器中的其他 Promise 风格的 API 一样,任何时候 WebAssembly 应用程序暂停时,除了浏览器的事件循环外,它都不会再次“唤醒”。这要求启动 WebAssembly 计算的 JavaScript 代码的执行本身返回到浏览器。
JavaScript 已经拥有完善的机制来表示异步计算:Promise 和async函数符号。JSPI 旨在与它很好地集成,而不是取代它。
JSPI 目前处于实验阶段——不应将其用于打算在生产中部署的 Web 应用程序。但是,这是标准的赛道努力;这意味着它最终将成为一个标准,我们希望它成为在所有主要浏览器中实施的标准。
这篇文章的其余部分将重点介绍如何访问 JSPI、如何开发使用它的代码以及一些要尝试的示例。
JSPI 正在 Intel x64 和 ARM 64 架构上开发。它适用于 Linux、macOS、Windows 和 ChromeOS。要在本地测试它,请转到chrome://flagsChrome,搜索“Experimental WebAssembly JavaScript Promise Integration (JSPI)”并选中该框。按照提示重启即可生效。
您应该至少使用版本110.0.5473.0(macOS) / 110.0.5469.0(Windows, Android) / 110.0.5478.4(Linux) 或 ChromeOS 来获取最新版本的 API。我们建议使用 Canary 通道以确保应用任何稳定性更新。此外,如果您希望使用 Emscripten 生成 WebAssembly(我们推荐),您应该使用至少3.1.28.
尚无法为最终用户启用该功能,只能通过启用此标志在本地进行测试。最终我们希望进行一次 Origin 试用,为想要选择加入的 Origin 启用此功能。
启用该标志后,您应该能够运行使用 JSPI 的脚本。下面我们展示了如何使用 Emscripten 在 C/C++ 中生成一个使用 JSPI 的 WebAssembly 模块。如果您的应用程序涉及不同的语言,例如不使用 Emscripten,那么我们建议您查看 API 的工作原理,您应该查看提案。
JSPI 的 Chrome 实现应该已经支持典型的用例。然而,它是实验性的,因此需要注意一些限制:
要查看所有这些工作,让我们尝试一个简单的示例。这个 C 程序以一种非常糟糕的方式计算斐波那契数列:通过要求 JavaScript 执行加法,甚至更糟糕的是使用 JavaScript Promises 来执行此操作:[2]
long promiseFib(long x) {
if (x == 0)
return 0;
if (x == 1)
return 1;
return promiseAdd(promiseFib(x - 1), promiseFib(x - 2));
}
// promise an addition
EM_ASYNC_JS(long, promiseAdd, (long x, long y), {
return Promise.resolve((k, r) => {
setTimeout(() => {
return k(x + y);
}, 0);
});
});
该promiseFib函数本身是 Fibonacci 函数的直接递归版本。有趣的部分(从我们的角度来看)是promiseAdd两个斐波那契半数相加的定义——使用 JSPI!。
我们使用EM_ASYNC_JSEmscripten 宏promiseFib在我们的 C 程序主体中将函数写为 JavaScript 函数。由于加法通常不涉及 JavaScript 中的 Promises,因此我们必须使用标准Promise.resolve函数来强制执行它。此外,我们必须隐藏setTimeout调用背后的算法,以确保引擎确实创建了一个涉及浏览器事件循环的 Promise。
该EM_ASYNC_JS宏生成所有必要的粘合代码,以便我们可以使用 JSPI 访问 Promise 的结果,就好像它是一个普通函数一样。
为了编译我们的小演示,我们使用 Emscripten 的emcc编译器:[3]
emcc -O3 badfib.c -o b.html -s ASYNCIFY=2
这会编译我们的程序,创建一个可加载的 HTML 文件 ( b.html)。这里最特别的命令行选项是-s ASYNCIFY=2. 这将调用生成代码的选项,该代码使用 JSPI 与返回 Promises 的 JavaScript 导入交互。[4]
如果将生成的b.html文件加载到 Chrome 中,那么您应该会看到类似于以下内容的输出:
fib(0) 0μs 0μs 0μs
fib(1) 0μs 0μs 0μs
fib(2) 0μs 0μs 3μs
fib(3) 0μs 0μs 4μs
…
fib(15) 0μs 13μs 1225μs
这只是前 15 个斐波那契数的列表,后面是计算单个斐波那契数所花费的平均时间(以微秒为单位)。每行的三个时间值指的是纯 WebAssembly 计算所用的时间,混合 JavaScript/WebAssembly 计算所用的时间,第三个数字给出了暂停版本的计算所用的时间。
请注意,这fib(2)是涉及访问 Promise 的最小计算,到计算时间为止,已经进行了fib(15)大约 1000 次调用。promiseAdd这表明 JSPI 函数的实际成本约为 1μs — 明显高于仅将两个整数相加,但远小于访问外部 I/O 函数通常所需的毫秒数。
在下一个示例中,我们将了解 JSPI 的一个令人惊讶的用途:动态加载代码。这个想法是fetch一个包含所需代码的模块,但要延迟到首次调用所需的函数。
我们需要使用 JSPI,因为 API 之类fetch的本质上是异步的,但我们希望能够从应用程序中的任意位置调用它们,特别是从调用尚不存在的函数的中间调用它们。
核心思想是用存根替换一个动态加载的函数;这个存根首先加载缺失的函数代码,用加载的代码替换自己,然后用原始参数调用新加载的代码。对该函数的任何后续调用都会直接转到加载的函数。此策略允许采用本质上透明的方法来动态加载代码。
我们要加载的模块非常简单,它包含一个返回的函数42:
// This is a simple provider of forty-two
#include
EMSCRIPTEN_KEEPALIVE long provide42(){
return 42l;
}
它在一个名为 的文件中p42.c,并使用 Emscripten 编译而没有构建任何“额外”:
emcc p42.c -o p42.wasm --no-entry -Wl,--import-memory
EMSCRIPTEN_KEEPALIVE前缀是一个 Emscripten 宏,确保provide42即使在代码中未使用该函数也不会被删除。这会生成一个 WebAssembly 模块,其中包含我们要动态加载的函数。
-Wl,--import-memory我们添加到 build of的标志p42.c是为了确保它可以访问与主模块相同的内存。[5]
为了动态加载代码,我们使用标准WebAssembly.instantiateStreamingAPI:
WebAssembly.instantiateStreaming(fetch('p42.wasm'));
该表达式用于fetch定位已编译的 Wasm 模块,WebAssembly.instantiateStreaming编译获取的结果并从中创建一个实例化模块。两者都fetch返回WebAssembly.instantiateStreaming承诺;所以我们不能简单地访问结果并提取我们需要的功能。相反,我们使用宏将其包装到 JSPI 样式的导入中EM_ASYNC_JS:
EM_ASYNC_JS(fooFun, resolveFun, (), {
console.log('loading promise42');
LoadedModule = (await WebAssembly.instantiateStreaming(fetch('p42.wasm'))).instance;
return addFunction(LoadedModule.exports['provide42']);
});
注意console.log调用,我们将使用它来确保我们的逻辑是正确的。
它addFunction是 Emscripten API 的一部分,但为了确保它在运行时对我们可用,我们必须告知emcc它是必需的依赖项。我们在以下行中执行此操作:
EM_JS_DEPS(funDeps, "$addFunction")
在我们想要动态加载代码的情况下,我们希望确保不会加载不必要的代码;在这种情况下,我们希望确保后续调用provide42不会触发重新加载。C 有一个简单的特性,我们可以为此使用:我们不provide42直接调用,而是通过 trampoline 调用,这将导致加载函数,然后,就在实际调用函数之前,将 trampoline 更改为绕过自身. 我们可以使用适当的函数指针来做到这一点:
extern fooFun get42;
long stub(){
get42 = resolveFun();
return get42();
}
fooFun get42 = stub;
从程序其余部分的角度来看,我们要调用的函数称为get42。它的初始实现是 via stub,它调用resolveFun实际加载函数。加载成功后,我们将 get42 更改为指向新加载的函数——并调用它。
我们的主函数调用get42了两次:[6]
int main() {
printf("first call p42() = %ld
", get42());
printf("second call = %ld
", get42());
}
在浏览器中运行它的结果是一个看起来像这样的日志:
loading promise42
first call p42() = 42
second call = 42
请注意,该行loading promise42只出现一次,而get42实际上被调用了两次。
这个例子演示了 JSPI 可以以一些意想不到的方式使用:动态加载代码似乎离创建承诺还有很长的路要走。此外,还有其他方法可以将 WebAssembly 模块动态链接在一起;这并不代表该问题的最终解决方案。
我们非常期待看到您可以使用这项新功能做什么!
#include
#include
#include
#include
typedef long (testFun)(long, int);
#define microSeconds (1000000)
long add(long x, long y) {
return x + y;
}
// Ask JS to do the addition
EM_JS(long, jsAdd, (long x, long y), {
return x + y;
});
// promise an addition
EM_ASYNC_JS(long, promiseAdd, (long x, long y), {
return Promise.resolve((k, r) => {
setTimeout(() => {
return k(x + y);
}, 0);
});
});
__attribute__((noinline))
long localFib(long x) {
if (x==0)
return 0;
if (x==1)
return 1;
return add(localFib(x - 1), localFib(x - 2));
}
__attribute__((noinline))
long jsFib(long x) {
if (x==0)
return 0;
if (x==1)
return 1;
return jsAdd(jsFib(x - 1), jsFib(x - 2));
}
__attribute__((noinline))
long promiseFib(long x) {
if (x==0)
return 0;
if (x==1)
return 1;
return promiseAdd(promiseFib(x - 1), promiseFib(x - 2));
}
long runLocal(long x, int count) {
long temp = 0;
for(int ix = 0; ix < count; ix++)
temp += localFib(x);
return temp / count;
}
long runJs(long x,int count) {
long temp = 0;
for(int ix = 0; ix < count; ix++)
temp += jsFib(x);
return temp / count;
}
long runPromise(long x, int count) {
long temp = 0;
for(int ix = 0; ix < count; ix++)
temp += promiseFib(x);
return temp / count;
}
double runTest(testFun test, int limit, int count){
clock_t start = clock();
test(limit, count);
clock_t stop = clock();
return ((double)(stop - start)) / CLOCKS_PER_SEC;
}
void runTestSequence(int step, int limit, int count) {
for (int ix = 0; ix <= limit; ix += step){
double light = (runTest(runLocal, ix, count) / count) * microSeconds;
double jsTime = (runTest(runJs, ix, count) / count) * microSeconds;
double promiseTime = (runTest(runPromise, ix, count) / count) * microSeconds;
printf("fib(%d) %gμs %gμs %gμs %gμs
",ix, light, jsTime, promiseTime, (promiseTime - jsTime));
}
}
EMSCRIPTEN_KEEPALIVE int main() {
int step = 1;
int limit = 15;
int count = 1000;
runTestSequence(step, limit, count);
return 0;
}
u42.cC 代码代表我们动态加载示例的主要部分:
#include
#include
typedef long (*fooFun)();
// promise a function
EM_ASYNC_JS(fooFun, resolveFun, (), {
console.log('loading promise42');
LoadedModule = (await WebAssembly.instantiateStreaming(fetch('p42.wasm'))).instance;
return addFunction(LoadedModule.exports['provide42']);
});
EM_JS_DEPS(funDeps, "$addFunction")
extern fooFun get42;
long stub() {
get42 = resolveFun();
return get42();
}
fooFun get42 = stub;
int main() {
printf("first call p42() = %ld
", get42());
printf("second call = %ld
", get42());
}
p42.c代码是动态加载的模块。
#include
EMSCRIPTEN_KEEPALIVE long provide42() {
return 42l;
}
由 Francis McCabe、Thibaud Michaud、Ilya Rezvov、Brendan Dahl 发表。
页面更新:2024-05-13
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号