TOC
ref
setup
首先需要安装 Emscripten
install emscripten
Emscripten 能够帮助我们将 C/C++ 代码编译为 WebAssembly 代码,同时帮助我们生成部分所需的 JavaScript 胶水代码
最简单的用法就是让 Emscripten 执行一段 C/C++ 代码,考虑如下一段代码
#include <stdio.h>#include <unistd.h>#include <fcntl.h>#include <sys/mman.h>#include <stdlib.h>
static void *f;
void open_file(const char *filename) { int fd = open(filename, O_RDWR); off_t size; if (fd < 0) { exit(1); } size = lseek(fd, 0, SEEK_END); f = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); if (f == (void *)(-1)) { exit(1); }}
int main() { printf("Hello World!\n"); open_file("story"); printf("story[%d] = %c\n", 7, ((char *)f)[7]); return 0;}
使用如下的命令编译,story
为一个文本文件
emcc hello.c -o hello.html --preload-file story
其中 --preload-file
会在编译时将文件打包进 Emscripten 虚拟出的内存文件系统中,以供代码进行 I/O 操作
https://emscripten.org/docs/porting/files/packaging_files.html#packaging-using-emcc
使用 js runtime 运行上述程序
> node hello.jsHello World!story[7] = w
上述代码的输出会输出在 console 中
另外也可以部署生成的 html 模板文件,其中会模拟出一个 terminal 显示相同的内容
cmake
为了与之前的项目对接,使用 cmake 构建出 wasm 和 js 代码,CMakeLists.txt
如下
cmake_minimum_required(VERSION 3.24)project(fat12-wasm-shell)
set(CMAKE_CXX_STANDARD 17)
add_executable(${PROJECT_NAME} wasm-shell.cpp ../core/core.cpp ../core/command.cpp ../utils/utils.cpp)set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS " \ -s EXPORTED_FUNCTIONS=\"['_Init','_ExecuteQuery','_free']\" \ -s EXPORTED_RUNTIME_METHODS=\"['ccall','cwrap','allocateUTF8','UTF8ToString']\" \ --preload-file a.img \")
add_definitions("-DWASM")
主要关注链接阶段的命令行选项
--preload-file
上文已经讲解过了EXPORTED_FUNCTIONS
配置在编译时需要暴露的函数,注意开头需要加上一个下划线,Init
和ExecuteQuery
是业务部分的接口,free
则是标准库函数,用于释放allocateUTF8
动态分配的内存EXPORTED_RUNTIME_METHODS
配置在运行时需要暴露的函数,此处可以参考 Interacting with code
在 build 文件夹下使用如下命令构建
emcmake cmake ..make
然后是封装好的 C/C++ 代码接口,以 Init
为例,为了避免 name mangling,需要使用 extern "C"
extern "C" {
auto Init() -> int { parser = std::make_shared<fat12_parser>("a.img"); // hard code return 0;}
}
最后是 html 文件,为了能够自定义 web shell,我们需要手写 html 文件,其中通过 JavaScript 胶水代码调用封装好的 C/C++ 代码接口
此处参考了 BusTub web shell,利用 jquery.terminal 模拟 terminal
比较 tricky 的地方 JS 代码如何调用 C++ 代码
Module['onRuntimeInitialized'] = function () { const executeQuery = Module.cwrap('ExecuteQuery', 'number', ['string', 'number', 'number']) const initialize = Module.cwrap('Init', 'number', []) window.executeQuery = (x) => { const bufferSize = 64 * 1024 let output = "\0".repeat(bufferSize) let ptrOutput = Module.allocateUTF8(output) const retCode = executeQuery(x, ptrOutput, bufferSize) output = Module.UTF8ToString(ptrOutput) Module._free(ptrOutput) return [retCode, output] } initialize()}
此处使用 cwrap
将 C++ 导出函数封装为 JS 函数
var func = Module.cwrap(ident, returnType, argTypes);
参数含义如下
- ident 导出函数的函数名,不含下划线前缀
- returnType 导出函数的返回值类型
- argTypes 导出函数的参数类型的数组
实际上 ExecuteQuery
的函数签名如下
auto ExecuteQuery(const char *input, char *output, [[maybe_unused]] u64 len) -> int;
- 一方面,从终端中获取到 input 后,直接传入
ExecuteQuery
ccall
和cwrap
封装函数会自动负责内存分配和释放问题,将 JS 字符串类型转换为char *
类型
- 另一方面,需要手动使用
allocateUTF8
方法动态分配内存,并使用UTF8ToString
将char *
类型转换为 JS 字符串类型,最后回收内存- 观察
cwrap
的对应参数类型为number
而非string
,可能原因是cwrap
会在栈上分配相应的空间,为了避免爆栈,需要手动管理内存 - 另外,JS 与 C++ 相互调用的时候,参数与返回值本质上都是
Number
类型
- 观察
- 注意最后一个参数为 u64 类型,否则无法进行无损类型转换,实际会发生传入的参数总为 0 的情形
- 此处大量参考了 https://cntofu.com/book/150/zh/ch2-c-js/readme.md
deploy
考虑使用 server-static 进行部署
npm install server-static -g
编写配置文件 static-server.config.js
如下
module.exports = { port: 4000, entry: "index.html", target: "http://localhost:8080", slient: true,};
然后键入 server-static
即可
部署在云服务器上,只需要如下文件
$ tree.├── fat12-wasm-shell.data├── fat12-wasm-shell.js├── fat12-wasm-shell.wasm├── index.html└── static-server.config.js
然后键入
nohup static-server > fat12-wasm-shell.log 2>&1 &
欢迎游玩 FAT12 Shell 🤣 (deprecated)
todo
- 上传自定义 fat12 镜像文件
- https://github.com/Cveinnt/LetsMarkdown.com