# Emscripten的文件系统 **Published by:** [wong2](https://paragraph.com/@wong2/) **Published on:** 2021-10-21 **URL:** https://paragraph.com/@wong2/emscripten ## Content Intro我们知道Emscripten可以将C/C++代码编译到WebAssembly,从而在浏览器中运行。为了实现这个目标,除了代码层面的编译外,Emscripten还需要在Web环境下提供大量对native runtime的模拟,如文件系统、底层图形库、网络等。今天重点看一下文件系统部分。Overview由于浏览器中的JavaScript无法直接访问操作系统的文件系统,Emscripten提供了一套虚拟文件系统来模拟一个POSIX FS。FileSystemArchitecture如上面的架构图所示,原生代码编写的应用使用libc/libxx中API进行文件系统操作,经Emscripten编译后,调用JavaScript编写的虚拟文件系统API。 默认的虚拟文件系统实现为MEMFS,顾名思义它是在内存里实现的,页面刷新后数据就会丢失。如果需要持久化,在浏览器里可以使用基于IndexedDB的IDBFS。使用示例下面我们通过一个例子来展示文件系统的使用,并为后续实验提供一个基础。C程序首先我们编写一个如下含有文件操作的C程序:#include <stdio.h> void append_line(char *filename, char *line) { FILE *fp = fopen(filename, "a"); fprintf(fp, "%s\n", line); fclose(fp); } int main() { return 0; } 这段程序中定义了 append_line 函数,它的作用就是往一个文件里添加一行新内容。我们在main中并没有调用它,而是准备暴露给JS代码调用。编译接下来我们用下面的命令编译这段C代码到WebAssembly,在选项中指定export append_line 函数。emcc fs.c \ -o fs.js \ -s EXPORTED_RUNTIME_METHODS="[cwrap, FS]" \ -s EXPORTED_FUNCTIONS="[_append_line]" 编译后会得到 fs.js 和 fs.wasm 两个文件,其中 fs.js 是 Emscripten提供的 wrapper 文件,它会自动处理 wasm 文件的加载等等。我们写一段极其简单的HTML来使用它:<body> <script src="fs.js"></script> <script> const appendLine = Module.cwrap('append_line', null, ['string', 'string']) </script> </body> 除了引入 fs.js 外,我们还通过 cwrap 把 append_line 转成了可以在JS中调用的函数。(C和JS代码如何互操作不是本文的重点,想了解更多可以阅读相关文档。使用我们在浏览器console里做些实验: 首先执行 appendLine('/tmp/test.txt', 'line1') 向一个文件里写入内容。 然后我们通过FS API 把内容读出来看看:> Module.FS.readFile('/tmp/test.txt', { encoding: 'utf8' }) "line1\n" 成功!Emscripten文件系统的实现对概念和使用有了基本认识后,我们来看看Emscripten文件系统的实现。WebAssembly调用JavaScript代码在前文架构图处提到,Emscripten的文件系统是在JavaScript层面实现的,然后由C/C++调用。更准确地说,是由C/C++编译后的WebAssembly调用。 我们以文本格式打开编译出的 fs.wasm,可以在头部看到一些 import 语句:(import "env" "__sys_open" (func $env.__sys_open (type $t1))) (import "wasi_snapshot_preview1" "fd_close" (func $wasi_snapshot_preview1.fd_close (type $t0))) (import "env" "__sys_fcntl64" (func $env.__sys_fcntl64 (type $t1))) (import "env" "__sys_ioctl" (func $env.__sys_ioctl (type $t1))) (import "wasi_snapshot_preview1" "fd_write" (func $wasi_snapshot_preview1.fd_write (type $t9))) (import "wasi_snapshot_preview1" "fd_read" (func $wasi_snapshot_preview1.fd_read (type $t9))) (import "env" "__sys_mkdir" (func $env.__sys_mkdir (type $t2))) (import "env" "emscripten_resize_heap" (func $env.emscripten_resize_heap (type $t0))) (import "env" "emscripten_memcpy_big" (func $env.emscripten_memcpy_big (type $t1))) (import "env" "setTempRet0" (func $env.setTempRet0 (type $t4))) (import "wasi_snapshot_preview1" "fd_seek" (func $wasi_snapshot_preview1.fd_seek (type $t7))) ... 不难看出,其中的 fd_read, fd_write 等是与文件操作相关的。它们的实现又在哪里呢? 要在WebAssembly中调用JavaScript代码,需要在实例化WebAssembly时传入一个 importObject。下面就是 fs.js中实例化的地方:return fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function (response) { var result = WebAssembly.instantiateStreaming(response, info); ... }) 其中 info 就包含了要被导入到WebAssembly实例的对象,它的定义也在 fs.js 里:var asmLibraryArg = { "__sys_fcntl64": ___sys_fcntl64, "__sys_ioctl": ___sys_ioctl, "__sys_open": ___sys_open, "emscripten_memcpy_big": _emscripten_memcpy_big, "emscripten_resize_heap": _emscripten_resize_heap, "fd_close": _fd_close, "fd_read": _fd_read, "fd_seek": _fd_seek, "fd_write": _fd_write, "setTempRet0": _setTempRet0 }; var info = { 'env': asmLibraryArg, 'wasi_snapshot_preview1': asmLibraryArg, }; 可以看到这里的定义和 fs.wasm 中的 import 是一一对应的。 搞懂了Emscripten编译出的WebAssembly如何调用JavaScript代码后,我们可以去看看具体的实现了。JavaScript实现Emscripten虚拟文件系统的实现位于源码的 src 目录下,主体是 library_fs.js,各个具体实现分别位于 library_memfs.js, library_idbfs.js 等。 打开 library_fs.js 可以看到里面定义了一个大的 FS 对象,上面定义了各种文件操作的方法,如创建文件、删除文件、查看目录等等,比如这是 readdir 的代码(选择它是因为比较短):readdir: function(path) { var lookup = FS.lookupPath(path, { follow: true }); var node = lookup.node; if (!node.node_ops.readdir) { throw new FS.ErrnoError({{{ cDefine('ENOTDIR') }}}); } return node.node_ops.readdir(node); } 这里node对应的数据结构是 FSNode,它表示的是文件系统树中的一个节点,有这样一些重要的属性:FSNode { id // 一个自增的数字 parent // 指向节点的父节点(root节点的父节点是自己) mode // 节点的类型及读写模式 name // 文件名或目录名 contents // 文件内容,如果节点是目录则是目录下文件名到子节点的map node_ops // 节点操作 } 其中 node_ops 的实现位于各个具体文件系统,比如我们可以在 library_memfs.js 里找到其 readdir 实现:readdir: function(node) { var entries = ['.', '..']; for (var key in node.contents) { if (!node.contents.hasOwnProperty(key)) { continue; } entries.push(key); } return entries; } 用于Node.js环境的library_nodefs.js 的实现则是基于Node.js的 fs 模块的:readdir: function(node) { var path = NODEFS.realPath(node); try { return fs.readdirSync(path); } catch (e) { if (!e.code) throw e; throw new FS.ErrnoError(NODEFS.convertNodeCode(e)); } } 就这样,FS 模块对外提供了统一的文件操作接口,且可以方便的切换到不同的底层实现。 现在我们回头看看前面提到的传给WebAssembly实例的asmLibraryArg,里面并没有直接使用FS模块的方法,而是出现了一个叫 _fd_write 的函数,它定义在 library_wasi.js 文件里。进而会发现它又通过系统调用 SYSCALLS.doWritev 才最终调用了 FS.write。 限于篇幅,关于 WASI 和 Emscripten 中对系统调用的实现这里就不作展开了。 ## Publication Information - [wong2](https://paragraph.com/@wong2/): Publication homepage - [All Posts](https://paragraph.com/@wong2/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@wong2): Subscribe to updates - [Twitter](https://twitter.com/wonderfuly): Follow on Twitter