使用来自WebasseMbly的异步Web API

2021-04-28 10:59:53

Web上的I / O API是异步的,但它们'在大多数系统语言中同步。将代码编译为webassembly时,您需要将一个API桥接到另一个 - 而且此桥接性是异步。在这篇文章中,您' ll学习何时以及如何使用Asyncify以及它在引擎盖下工作。

我' ll从c的一个简单的例子开始。说,你想从文件中读取用户' s名称,并用一个&#34迎接它们;你好,(用户名)!"信息:

#包括< stdio.h> int main(){file * stream = fopen(" name.txt"," r"); CHAR名称[20 + 1]; size_t len = fread(&名称,1,20,流);名称[len] =' \ 0' ; fclose(流); Printf("你好,%s!\ n",name);返回0; }

虽然该示例不做多少事情,但它已经展示了你的东西' LL在任何大小的应用中找到的东西:它读取了外部世界的一些输入,内部处理它们并将输出写回外部世界。所有与外界的互动都会通过一些称为输入输出函数的少数功能发生,也缩短到I / O.

要从C中读取名称,您需要至少两个关键的I / O呼叫:Fopen,打开文件,并释放读取数据。检索数据后,您可以使用另一个I / O功能PrintF将结果打印到控制台。

这些功能乍一看看起来非常简单,你不必三思而后行地思考读取或写入数据的机器。但是,根据环境,内部可能会有很多:

如果输入文件位于本地驱动器上,则应用程序需要执行一系列内存和磁盘访问以找到文件,检查权限,打开它以读取,然后按块读取块,直到检索请求的字节数。 。这可能很慢,具体取决于磁盘的速度和所请求的大小。

或者,输入文件可能位于安装的网络位置,在这种情况下,现在涉及网络堆栈,增加每个操作的复杂性,延迟和潜在重试的数量。

最后,甚至PrintF不保证将事物打印到控制台,并且可能被重定向到文件或网络位置,在这种情况下它必须通过上面的相同步骤进行。

长话短说,I / O可以很慢,你可以' t预测特定呼叫在代码上快速浏览需要多长时间。虽然该操作正在运行,但您的整个应用程序将出现冻结并对用户没有响应。

这不限于C或C ++。大多数系统语言以同步API的形式呈现所有I / O.例如,如果将示例转换为生锈,则API可能看起来更简单,但应用相同的原则。您只需拨打电话并同步等待它返回结果,而它执行所有昂贵的操作,最终返回一个调用中的结果:

但是,当您尝试将这些样本中的任何一个样本编译为WebasseMbly并将其转换为Web时会发生什么?或者,提供一个具体的例子,可以"文件读取"操作转换为?它需要从某些存储中读取数据。

Web具有各种不同的存储选项,您可以映射到内存存储(JS对象),LocalStorage,IndexedDB,服务器端存储以及新文件系统访问API。

但是,只有两个API-Memory存储和LocalStorage - 可以同步使用,并且两者都是您可以存储的最限制性的选项以及多长时间。所有其他选项只提供异步API。

这是网络上执行代码的核心属性之一:任何耗时的操作,包括任何I / O,必须异步。

原因是Web是历史上单线程的,并且触摸UI的任何用户代码都必须在与UI相同的线程上运行。它必须与其他重要任务相竞争,如布局,渲染和事件处理CPU时间。你想要一块javascript或webassembly,以便能够启动一个"文件读取"操作和阻止所有其他选项卡,或者,过去,整个浏览器 - 从毫秒到几秒钟,直到它' s结束。

相反,代码仅允许将I / O操作加上一旦&#39完成了一次以执行的回调。此类回调是作为浏览器的一部分执行的。我赢了'如果你'' revery' verfy'遗嘱感兴趣地学习事件循环如何在引擎盖下工作,签出讲述这个主题的任务,微量障碍,队列和计划深入。

简短的版本是浏览器通过将它们从队列中从队列中取出一个无限循环中的所有代码。当触发某些事件时,浏览器队列相应的处理程序,并在下一个循环迭代中,从队列中取出并执行。此机制允许在仅使用单个线程时模拟并发性和运行大量并行操作。

要记住此机制的重要事项是,虽然您的自定义JavaScript(或Webassembly)代码执行,但事件循环被阻止,虽然它是,但没有办法对任何外部处理程序,事件,I / O作出反应。获得I / O结果的唯一方法是注册回调,完成执行代码,并将控件返回给浏览器,以便它可以继续处理任何挂起的任务。一旦I / O完成,您的处理程序将成为其中一个任务,并将执行。

例如,如果您想要在现代JavaScript中重写上述样本并决定从远程URL读取名称,则您将使用fetch API和Async-Await语法:

async函数main(){让响应=等待获取(" name.txt");让名称=等待答复。文本 ( ) ;安慰 。日志("您好,%s!",名称); }

即使它看起来同步,在引擎盖下,每个等待都是基本上是标准的语法糖:

在该替换示例中,这是一个比特更清晰的示例,启动请求,并使用第一回调订阅响应。一旦浏览器收到初始响应 - 只需http标题 - 它异步调用此回调。回调开始读取身体作为使用response.text()的文本,并使用另一个回调订阅结果。最后,一旦获取已经检索了所有内容,它会调用最后一个回调,打印"你好,(用户名)!"到控制台。

由于这些步骤的异步性质,原始函数一旦安排I / O,就可以立即将控制返回到浏览器,并响应整个UI响应,包括其他任务,包括渲染,滚动等等,而且I / O在后台执行。

作为最终的示例,甚至是简单的API,"睡眠",它使应用程序等待指定的秒数,也是I / O操作的一种形式:

当然,您可以以非常简单的方式翻译它,这将阻止当前线程直到时间到期:

事实上,'究竟是什么在其默认实施中的"睡眠",但是' s非常低效,将阻止整个UI并赢得任何其他事件同时处理。一般来说,在生产代码中on don'

相反,更加惯用的版本和#34;睡眠"在JavaScript中将涉及调用setTimeout(),并使用处理程序订阅:

所有这些例子和API的共同点是什么'在每种情况下,原始系统语言中的惯用代码使用I / O的阻塞API,而等效示例用于Web使用异步API。在编译到Web时,您需要在这两个执行模型之间以某种方式转换,并且WebAsseMbly没有内置的能力才能执行此操作。

这是Asyncify进出的地方。Asyncify是EMScripten支持的编译时功能,允许暂停整个程序并在稍后异步恢复它。

如果您想在最后一个示例中使用Asyncify来实现异步睡眠,则可以这样做:

#包括< stdio.h> #包括< emscripten.h> em_js(void,async_sleep,(int copes),{asyncify。handlesleep(wakeup => {setsimout(唤醒,秒* 1000);}); ......放(" a"); async_sleep(1);放(" b");

EM_JS是一种宏,允许定义JavaScript片段,就像它们是C函数一样。在内部,使用函数asyncify.handlesleep(),它告诉EMScripten暂停程序并提供一旦异步操作完成后应调用的唤醒()处理程序。在上面的示例中,处理程序将传递给SetTimeOut(),但它可以在接受回调的任何其他上下文中使用。最后,您可以像常规睡眠()或任何其他同步API一样调用您想要的Async_sleep()。

编译此类代码时,您需要告诉EMScripten来激活Asyncify功能。通过assyncify和syyncify_imports = [func1,func2]来执行此操作,其中包含可能是异步的数组的函数列表。

这让Emscripten知道对这些函数的任何调用可能需要保存和恢复状态,因此编译器将在此类调用周围注入支持代码。

现在,当您在浏览器中执行此代码时,请参阅像您的无缝输出日志,如您所期望的无缝输出日志。

您也可以从Asyncify函数中返回值。您需要做的是返回Handlesleep()的结果,并将结果传递给WakeUp()回调。例如,如果不是从文件读取,则要从远程资源获取一个数字,可以使用下面的片段发出请求,暂停C代码,并重新检索响应主体 - 所有如何无缝完成,就像呼叫同步一样。

em_js(int,get_answer,(),{return asyncify。handlesleep(wakeup => {fetch(" ackan.txt")。然后(response =>响应。text()。然后(文本=>唤醒(数字(文本)));});});放("得到答案..."); int回答= get_answer(); printf("答案是%d \ n",答案);

事实上,对于基于承诺的API,如fetch(),您甚至可以使用JavaScript' s异步 - 等待特征而不是使用基于回调的API来组合Asyncify。为此,而不是Asyncify.Handlesleep(),call asyncify.handleasync()。然后,不必计划唤醒()回调,您可以通过异步JavaScript函数并使用等待和返回内部,使代码看起来更自然和同步,同时不会丢失异步I / O的任何优势。

em_js(int,get_answer,(),{return asyncify。handleasysync(async()=> {让响应=等待获取(" ackan.txt");让文本=等待响应。文字();返回号码(文本);});}); int回答= get_answer();

但此示例仍然仅限于数字。如果您想实现原始示例,我试图从文件中获取用户的名称,从中何时才能成为字符串?好吧,你也可以这样做!

EMScripten提供称为Jualind的功能,允许您处理JavaScript和C ++值之间的转换。它也支持异步,因此您可以在外部承诺上调用await(),它将类似于Async-Await JavaScript代码的等待:

使用此方法时,您甚至需要将Asyncify_import传递为编译标志,默认情况下已包含'默认情况下。

例如,您在生锈代码中有类似的同步调用,您要将您想要映射到Web上的异步API。事实证明,你也可以这样做!

首先,您需要将这样的函数定义为常规导入通过extern块(或您所选择的语言'对外部函数的语法)。

现在,您需要使用代码介绍WebasseMbly文件以存储/还原堆栈。对于C / C ++,Emscripten会为我们做到这一点,但它在这里没有使用它,因此该过程有点手动。

幸运的是,Asyncify变换本身是完全的工具链无症状。它可以改变任意的webassembly文件,无论哪个编译器它'该转换是单独提供的,作为来自Binaryen Toolchain的WASM-Opt优化器的一部分,可以像这样调用:

pass -ascify以启用转换,然后使用--pass-arg = ...以提供逗号分隔的异步函数列表,其中程序状态应暂停,稍后恢复。

left是提供支持的运行时代码,实际执行该挂起并恢复WebasseMbly代码。同样,在C / C ++案例中,EMScripten将包括在内,但现在您需要处理任意WebasseMbly文件的自定义JavaScript胶水代码。我们' ve为此创建了一个图书馆。

它模拟了标准的WebAsseMbly Instantial API,但在其自己的命名空间下。唯一的区别是,在常规webassembly api下,您只能将同步函数提供为导入,而在Asyncify包装器下,您也可以提供异步导入:

const {实例} =等待Asyncify。 InstantiAtesteStreaming(获取(' app.wasm'),{enth:{async get_answer(){让响应=等待获取(" acquess.txt");让文本=等待响应。文字();返回号码(文本);}}}); ......等待实例.ports。主要的 ( ) ;

一旦您尝试称之为上面的示例中的异步函数 - 像get_answer() - 从webassembly侧,库将检测到返回的承诺,暂停和保存WebasseMbly应用程序的状态,订阅承诺完成,并稍后,一旦它解决了,就解决了,无缝恢复呼叫堆栈和状态,并继续执行,好像没有发生任何事情。

由于模块中的任何函数都可能进行异步呼叫,因此所有导出都会变得异步,因此它们也会被包裹。在上面的示例中,您可能已经注意到,您需要等待instance.exports.main()的结果,以知道执行何时何时完成。

当Asyncify检测到一个Asyncify_imports函数的呼叫时,它启动异步操作,保存应用程序的整个状态,包括调用堆栈和任何临时当地人,稍后,当该操作完成时,恢复所有内存并呼叫堆叠并从同一个地方恢复,并且具有与程序从未停止的相同状态。

这与我之前显示的JavaScript中的异步 - 等待特征非常相似,但与JavaScript One不同,不需要语言的任何特殊语法或运行时支持,而是通过在编译时转换普通同步功能 - 而是适用时间。

Asyncify采用此代码并将其转换为大致相当于以下一个(伪代码,实际转换比此更涉及):

if(mode == normal_execution){puts(" a"); async_sleep(1); Savelocals(); mode =展开;返回 ; }如果(mode == rewinding){restorelelocals(); mode = normal_execution;放(" B");

最初模式设置为romalm_execution。相应地,第一次执行此类转换代码,只执行通往Async_sleep()的部分将被评估。一旦安排了异步操作,Asyncify会保存所有当地人,并通过从每个函数返回到顶部来解除堆栈,以这种方式使控制回浏览器事件循环。

然后,一旦Async_sleep()解析,Asyncify支持代码将更改模式以重新运行,并再次调用该函数。这次,"正常执行"分支是跳过 - 因为它已经上次已经完成了这项工作,我想避免印刷" a"两次 - 而且它直接到了"倒下"分支。一旦它到达,它达到了,它会恢复所有存储的当地人,更改模式回到"正常"并继续执行,好像代码永远不会停止。

不幸的是,异步转换为完全自由,因为它必须注入相当多的支持代码来存储和恢复所有这些本地,从而在不同模式下导航呼叫堆栈等。它试图仅在命令行上修改标记为异步的函数,以及其潜在的呼叫者中的任何一个,但在压缩之前,代码大小仍可能会增加大约50%。

这是' t的理想,但在许多情况下,当替代方案没有完全或必须对原始代码进行重写的功能时,可以接受。

确保始终为最终构建启用优化以避免它更高。您还可以通过仅限于指定函数和/或仅直接函数调用,检查特定于特定于特定的优化选项以减少开销。运行时性能也有次要的成本,但它' s仅限于异步呼叫自己。但是,与实际工作的成本相比,它通常可以忽略不计。

现在你看了看起来简单的例子,我' ll继续转移到更复杂的情景。

如文章开头所述,Web上的一个存储选项是异步文件系统访问API。它可以从Web应用程序提供对真实主机文件系统的访问。

另一方面,在控制台和服务器端中存在一个名为WEDAssembly I / O的WASI的De-Facto标准。它被设计为系统语言的编译目标,并以传统的同步形式公开各种文件系统和其他操作。

如果你可以将一个映射到另一个人怎么办?然后,您可以使用支持WASI目标的任何源语言中的任何源语言编译任何应用程序,并在Web上的沙箱中运行它,同时允许它在真实的用户文件上运行!使用异步,您可以做到这一点。

在这个演示中,i' Ve编译的rust coreutils用几个小修补程序向isi括号,通过Asyncify Transform传递,并通过WASI实现了JavaScript侧的文件系统访问API的异步绑定。一旦与Xterm.js终端组件组成,它就提供了一个现实的shell在浏览器选项卡中运行并在真实的用户文件上运行 - 就像一个实际的终端。

Asyncify使用情况不仅限于定时器和文件系统。您可以进一步进一步并在Web上使用更多的利基API。

例如,也在Asyncify的帮助下,它可以映射Libusb-可能是最受欢迎的本机库,用于使用USB设备 - 到WebUSB API,它为Web上的这些设备提供异步访问。一旦映射和编译,我得到了标准的libusb测试和示例,以便在网页的沙箱中运行的所选设备。

这些示例似乎只有强大的Asyncify可以用于拓展间隙并将各种应用程序移植到Web,允许您获得跨平台访问,沙箱和更好的安全性,而不是失去功能。