WebAssembly 和 Emscripten

邹业盛 2022-05-17 00:50 更新
  1. 是什么东西?
  2. C 编译
  3. 例子,浏览器中嵌入 Python
  4. 相互调用背后的东西
  5. 网络 IO 和异步
  6. 可能的应用场景

1. 是什么东西?

WebAssembly 是一种字节码格式,执行它需要对应的一种虚拟机。

我们一般不会手写 WebAssembly ,而是通过其它高级语言,比如 C ,Rust,Go 等,利用编译器来得到 WebAssembly 二进制格式的输出。

执行 WebAssembly 需要的虚拟机,在不同的环境下,有不同的实现,比如,在浏览器中。当然,也有独立的实现,像:https://github.com/bytecodealliance/wasmtime

WebAssembly 也有对应的文本格式,是采用类似 Lisp 语言的 S 表达式来组织的,可以手写它( wasmtime 也支持直接执行),比如下面这段:

(module
    (import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
    (memory 1)
    (export "memory" (memory 0))
    (data (i32.const 8) "hello world\n")
    (func $main (export "_start")
        (i32.store (i32.const 0) (i32.const 8))
        (i32.store (i32.const 4) (i32.const 12))
        (call $fd_write
            (i32.const 1)
            (i32.const 0)
            (i32.const 1)
            (i32.const 20)
        )
        drop
    )
)

把这些内容保存成 demo.wat ,用前面的 wasmtime 执行就可以输出 hello world

wasmtime demo.wat

这里面除了 WebAssembly 语言本身的语法格式外,还有像 fd_write 这些需要导入的函数。只有具体的虚拟机,或者说运行时环境,提供了这些函数,这段代码才可以正常运行,进一步才能去完成更业务性的事,否则可能只能一直做些四则运算了。

像运行时的“标准函数”,现在还不是一个稳定的标准,在 https://github.com/WebAssembly/WASI 这里,能看到它们在被缓慢的推进着,一份简单的文档在 https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md

不需要尝试去找针对 WebAssembly 的文本格式的完整的“语法手册”,因为根本找不到。官方的“语法手册”,是从 Token 层面说的,给编译器开发人员看的那类文档,手写 S 表达式不是 WebAssembly 的使用方式。

我们会使用其它的高级语言,通过编译得到 WebAssembly 。之后的重点,其实已经不在 WebAssembly 这个语言本身,反正是编译器的事。相l对的,了解各个运行时环境,知道它们提供了哪些像前面的 fd_write 那样的实现,及怎么和 js 交互,才更有意义。

2. C 编译

2.1. 安装

emscripten ,是一套编译构建方案,同时提供了比较完整的 SDK ,它在 https://emscripten.org/ 。它使我们可以非常方便地使用 C 语言完成 WebAssembly 相关的开发与环境集成。

安装的方式,是先拉取 git 代码:

git clone https://github.com/emscripten-core/emsdk.git

进入目录后,执行安装,它会下载 C 编辑器,nodejs 等一堆东西:

cd emsdk
./emsdk install latest
./emsdk activate latest

最后处理一下环境(效果只在终端的当前会话有效):

source ./emsdk_env.sh

emcc 是一个可执行命令时,整个环境就准备好了。

2.2. Hello World

先写个 hello world

#include <stdio.h>

int main() {
    printf("Hello, World\n");
    return 0;
}

前面的环境如果处理好了,就可以用 emcc 去直接编译这个 demo.c

emcc demo.c

编译的结果是在当前目录下,会有 a.out.jsa.out.wasm 两个文件。

a.out.wasmWebAssembly 的二进制是格式的代码。

a.out.js 处理加载和额外的依赖,同时,它支持 nodejs 。所以直接执行:

node a.out.js

就可以看到 Hello, World 的输出。

这里,如果用前面的 wasmtime 去执行 a.out.wasm ,会看到报错,报措的原因是找不到一些定义。这就是前面说过的,语言问题是小事,麻烦的是环境。

emcc 做了一套方案,去尽量兼容 C 的开发与编译环境。当开发中需要的一些依赖要跑在 nodejs ,或者浏览器中的时候,环境相关的东西就需要被重新解释,比如“标准输出”变成 console.log 。这些“解释”,不是 WebAssembly 语言范畴的,要么运行时提供内置实现,比如 wasmtime 里面可能提供了一些。要么运行时提供一个注入式的方案,比如 nodejs 或者浏览器上,都可以完成 WebAssemblyjavascript 的互调用,那么“标准输入”在编译时就可以仅被解析成任何的一个符号,执行时再由 javascript 提供这个符号的实现就可以了。

wasmtime a.out.wasm 的报错,是找不到 env::emscripten_memcpy_big ,其实打开 a.out.js 找一下,能找到 emscripten_memcpy_big ,我个猜的话, emcc 就是使用 js 提供了一些 WebAssembly 的运行依赖。

a.out.js 中的“标准输出”,就是 _fd_write 这个函数,有兴趣的可以加一些 console.log 看看。

nodejs 可以执行 WebAssembly ,浏览器也可以,为了方便,编译的方式调整一下:

emcc demo.c -o hello.html

这样的话,除了 hello.wasm , hello.js ,还会有一个 hello.html 的 html 文件。

可以直接执行:

node hello.js

也可以启动一个静态服务,在浏览器中访问这个 hello.html

python -mhttp.server

通过访问 http://localhost:8000/hello.html ,就可以看到下图了:

hello.html ,别看它有很多行代码,其中有好多是 svg 图 …… )

2.3. js 调用 c

假设要做一个 C 版本的 add 函数给 js 调用,在 demo.c 文件中:

int add(int a, int b) {
    return a + b;
}

emcc 提供的编译工具,封装了 js 调用 WebAssembly 的过程,只需要在编译时使用 EXPORTED_FUNCTIONS 选项:

emcc demo.c -o hello.html -sEXPORTED_FUNCTIONS=_add

同样,打开 http://localhost:8000/hello.html 之后,在控制台输入:

Module._add(1,2)

就能看到 3 的返回。 Moduleemcc 定义的全局量。

如果要把 Module 作为模块,而不是全局量,在 emcc 编译时加一个参数就可以了:

emcc demo.c -o demo.js -sEXPORTED_FUNCTIONS=_add  -sMODULARIZE

这样, demo.js 的加载会返回一个 Promise 对象, resolveModule

var f = require('./demo.js')
f().then(ins => console.log(ins._add(1,1)))
2

如果觉得 -sEXPORTED_FUNCTIONS 麻烦,也可以在源码中使用 EMSCRIPTEN_KEEPALIVE 宏直接“标记”要导出的函数:

#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int add_1(int a) {
    return a + 1;
}

这样,直接编译就可以导出 add_1 这个函数了:

emcc demo.c -o demo.html
emcc demo.c -o demo.js -sMODULARIZE

2.4. 参数传递,传入字符串

因为 WebAssembly 本身设计上的限制,除了 intfloat 可以作为值直接传递之外,其它的类型,比如字符串,数组,都只能通过地址来完成传递。这点上,倒是和 C 语言的机制很搭。

下面是一个 str_len 的例子,传入一个字符串,返回这个字符串的长度:

#include <stdio.h>
#include <string.h>

#ifdef __EMSCRIPTEN__
    #include <emscripten.h>
#else
    #define EMSCRIPTEN_KEEPALIVE ;
#endif


EMSCRIPTEN_KEEPALIVE
int str_len(char* s) {
    return strlen(s);
}

#ifndef __EMSCRIPTEN__
int main() {
    printf("%d\n", str_len("hello"));
    return 0;
}
#endif

如果 js 中这样调用:

const demo = require('./demo');

demo().then( cbind => {
  const n = cbind._str_len("Hello11111111");
  console.log(n);
});

结果是不对的。我猜,js 中的字符串传入的时候, emcc 会把它当地址(指针)处理,所以这里看到的是 4 。如果希望把它当成字符串,而不是地址,那么 js 中的入参,到 C 函数的调用,参数在中间需要做显式的格式转换声明,这个过程, emcc 通过 js 的 ccall 函数提供支持。为了使用 ccall 函数,编译时需要把 ccall 加入:

emcc demo.c -o demo.js -sMODULARIZE -sEXPORTED_RUNTIME_METHODS=ccall

ccall 的用法: ccall('func_name', 'return type', ['arg1 type', 'arg2 type'], [arg1, arg2])

把原来的 str_len 函数包装一下就可以得到正确的结果了:

const demo = require('./demo');

demo().then( cbind => {
  const n = cbind._str_len("Hello11111111");
  console.log(n);
  const l = cbind.ccall('str_len', 'number', ['string'], ["hello11111111"]);
  console.log(l);
});

除了 stringccall 还可以支持 number, array (整数), boolean

2.5. 参数传递,传出字符串

这里做一个随机字符串的例子:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <string.h>

#ifdef __EMSCRIPTEN__
    #include <emscripten.h>
#else
    #define EMSCRIPTEN_KEEPALIVE ;
#endif


const int MAX_LEN = 100;

int c_get_str(char* s) {
    srand(time(NULL));
    int r = rand() % MAX_LEN;
    int i;
    for(i = 0; i < r; i++) {
        s[i] = 'M';
    }
    return r;
}


EMSCRIPTEN_KEEPALIVE
int getRandomStr(char* s) {
    return c_get_str(s);
}


#ifndef __EMSCRIPTEN__
int main() {
    char* src = (char*)malloc(MAX_LEN);
    int len = getRandomStr(src);
    char* dest = (char*)malloc(len + 1);
    strcpy(dest, src);
    free(src);
    dest[len] = '\n';
    printf("%s", dest);
    free(dest);
    return 0;
}
#endif

代码比较简单,就是随机生成 N 个 M 字符。

要在 js 中使用 getRandomStr 函数,除了前面说的那些东西,在 emcc 编译时,还需要导出额外的支持函数(这些函数使用比较普遍):

emcc demo.c \
    -o demo.js \
    -sMODULARIZE \
    -sEXPORTED_RUNTIME_METHODS=UTF8ToString \
    -sEXPORTED_FUNCTIONS=_malloc,_free

2.6. 参数传递,传出结构体

结构体本身只是一段内存,所以在 js 中处理结构体,通用的方法是“解析结构体的内存”。但在实践中,可以避免这些麻烦的处理,直接用 C 定义相关的方法就可以了:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#ifdef __EMSCRIPTEN__
    #include <emscripten.h>
#else
    #define EMSCRIPTEN_KEEPALIVE ;
#endif


typedef struct {
    char* name;
    char* desc;
    int age;
} Person;


EMSCRIPTEN_KEEPALIVE
void get_person(Person* p) {
    p -> name = "hello";
    p -> desc = "world";
    p -> age = 22;
}

EMSCRIPTEN_KEEPALIVE
char* get_person_name(Person *p) {
    return p -> name;
}


EMSCRIPTEN_KEEPALIVE
int get_person_type_size() {
    return sizeof(Person);
}


#ifndef __EMSCRIPTEN__
int main() {
    Person* p = malloc(sizeof(Person));
    get_person(p);
    printf("%s", p -> name);
    free(p);
    return 0;
}
#endif

上面的代码中,对于 Person 这个结构体,如果要获取它的名字,单独定义一个 get_person_name 函数总是可行的。

也顺便介绍一下通用的办法,是使用 emcc 提供的 getValue 函数,直接按数据类型,一块一块地读内存结构。要使用 getValue ,编译时记得将它导出:

emcc demo.c \
    -o demo.js \
    -sMODULARIZE \
    -sEXPORTED_RUNTIME_METHODS=UTF8ToString,getValue \
    -sEXPORTED_FUNCTIONS=_malloc,_free

在 js 中:

const demo = require('./demo');

demo().then( cbind => {
  const n = cbind._get_person_type_size();
  // console.log(n);
  const buffer = cbind._malloc(n);
  cbind._get_person(buffer);
  // console.log(cbind.UTF8ToString(cbind._get_person_name(buffer)));

  const name = cbind.getValue(buffer + 4, 'i32')
  console.log(cbind.UTF8ToString(name));
  const age = cbind.getValue(buffer + 8, 'i32')
  console.log(age);
});

这里直接就知道 +4+8 的偏移,是建立在完全了解 Person 结构定义前提下的。

2.7. 参数传递,传入结构体

结构体是连续内存地址,前面有 getValueUTF8ToString ,自然也有对应的 setValuestringToUTF8 ,它们可以用来拼出指定内存地址上的数据状态,进而就可以实现结构体的传入。

在前面 C 代码的基础上,添加一个函数,用于打印 Person 的信息:

EMSCRIPTEN_KEEPALIVE
void print_person(Person *p) {
    printf("%s\n", p -> name);
    printf("%s\n", p -> desc);
    printf("%d\n", p -> age);
}

编译时把 setValuestringToUTF8 加上,还有一个 lengthBytesUTF8 工具,用来计算字符串的字节数:

emcc demo.c \
    -o demo.js \
    -sMODULARIZE \
    -sEXPORTED_RUNTIME_METHODS=UTF8ToString,getValue,setValue,stringToUTF8,lengthBytesUTF8 \
    -sEXPORTED_FUNCTIONS=_malloc,_free

然后尝试从 js 调用 print_person 函数:

const demo = require('./demo');

demo().then( cbind => {
  const n = cbind._get_person_type_size();
  const buffer = cbind._malloc(n);

  const name = "FROM JS";
  const nameLen = cbind.lengthBytesUTF8(name) + 1;
  const nameBuffer = cbind._malloc(nameLen);
  cbind.stringToUTF8(name, nameBuffer, nameLen);

  const desc = "HAHAHA";
  const descLen = cbind.lengthBytesUTF8(desc) + 1;
  const descBuffer = cbind._malloc(descLen);
  cbind.stringToUTF8(desc, descBuffer, descLen);

  cbind.setValue(buffer, nameBuffer, 'i32');
  cbind.setValue(buffer + 4, descBuffer, 'i32');
  cbind.setValue(buffer + 8, 88, 'i32');

  cbind._print_person(buffer);

  cbind._free(descBuffer);
  cbind._free(nameBuffer);
  cbind._free(buffer);
});

基本过程,就是针对字符串的数据,需要为它们先单独分配空间,再把空间地址通过 setValue 设置到原地址上。

2.8. 其它编译

前面说的 emcc 也是支持 c++ 的。市面上还有 go, rust, 类 typescript 等其它实现。不过涉及直接和内存概念打交道,还是 C 简单直接。

3. 例子,浏览器中嵌入 Python

有了 C 的支持,很多东西都可以移植到浏览器跑了,从小的一些加密,散列算法,到大的完整的应用程序(单纯的计算算法,很多都能找到 js 的实现,从实际场景来说不一定非要用 C 的)。

我在考虑做一个什么样的例子的时候,这个问题纠结了不少时间,后来看到 MicroPython 中有现成的 js 移植,还直接用的就是 emcc ,觉得直接用它实现一个小功能就是很合适的。(网上还能找到一个法国大叔的 brython 的项目,是用 js 实现了把 Python 代码转换成 js 代码,也是狠)

这里,打算用 Python 作为配置语言,使用它完成一套 json 字符串配置的输出。

比如,系统需要的输入是:

{
    "name": "Hello",
}

现在引入 Python ,可以做成(随便想的,实现时再调整):

import random

class Config(BaseConfig):
    name = random.choice(["Hello", "World"])

js.output(Config().as_config())

加入随机生成的功能,每次执行,都能观察到实际效果的变化,对于调试和检查是很方便的。

3.1. 安装

emcc 安装好,并且它的环境变量也处理之后(source emsdk_env.sh),直接在 MicroPythonports/javascript 目录中 make 就可以编译 MicroPython 的 js 移值这部分。

编译完成之后,会有一个 build 目录,里面的 micropython.jsfireware.wasm 是最终有用的。

3.2. 用 js 实现 C 的 api

可以用 js 实现 C 中调用的一个函数。原理,大概就是 C 编译到 WebAssembly 时,根据函数名事先在地址上留了坑位,比如只在头文件中声明了函数签名,运行时再用 js 实现的函数运行得到结果。当然,参数类型有限制,毕竟 WebAssembly 只有整型和浮点是直接的值传递。

上面是 MicroPython 自己的,移值到 js 时,对于“标准输出”的处理过程。

3.3. 自定义一个 Python 模块

Python 和 js 之间,需要有一个信息传递的渠道,标准输出本来是现成的一个( MicroPython 是用确定 ID 的 DOM 节点的 print 事件处理),但是考虑到,写 Python 代码的人,也需要有调试的手段,标准输出应该用在这里。那么业务场景需要的 Python 和 js 之间的信息传递,就需要自己再造一个了。(跨了不同环境,就不是简单的一个 return 能搞定的了)

这里要做的一个模块,就是实现 Python 和 js 之间信息传递的功能。

先不考虑 js 调用的事,只看怎么用 C 实现一个 Python 的模块。按文档,自定义一个模块,需要做两步:

假设期望的模块使用是:

import js
js.emit("string")

即, js 模块,里面有一个 emit 方法,参数是字符串。

多说一点,这里使用单独模块,设计上优于直接添加 builtin 方法/模块。如果 builtin 的话,代码可能是:

jsemit("string")

看着像是少了一个 import 的动作,实际上这直接造成代码的无法兼容, jsemit("string") 无法在标准的 Python 环境下执行。但如果是单独的 js 模块,可以在标准的 Python 环境下用 python 代码先定义一个这样的模块,那么同样的代码既可以在 MicroPython 环境下执行,也可以在标准的 Python 环境下执行。所以不要随便动 builtin

下面开始做模块的开发。

新建一个 modjs.c 文件:

#include "py/runtime.h" 
#include "py/objstr.h"

STATIC mp_obj_t js_emit(const mp_obj_t str) {
    const char* s = mp_obj_str_get_str(str);
    mp_printf(&mp_plat_print, s);
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(js_emit_obj, js_emit);

STATIC const mp_rom_map_elem_t js_module_globals_table[] = {
    { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_js) },
    { MP_ROM_QSTR(MP_QSTR_emit), MP_ROM_PTR(&js_emit_obj) },
};
STATIC MP_DEFINE_CONST_DICT(js_module_globals, js_module_globals_table);

const mp_obj_module_t js_module = {
    .base = { &mp_type_module },
    .globals = (mp_obj_dict_t *)&js_module_globals,
};

MP_REGISTER_MODULE(MP_QSTR_js, js_module, MICROPY_PY_JS);

这里的一些处理是基于名字的,像 js_emit_objMP_DEFINE_CONST_FUN_OBJ_1 ,它们在编译时,有宏会按参数展开并完成定义行为。

最后一行的 MICROPY_PY_JS 也是一个宏,需要事先在 mpconfigport.h 中添加它:

#define MICROPY_PY_JS               (1)

当然,直接写死 1 也是可以的。定义 MICROPY_PY_JS 只是为了好控制。

最后,要修改 Makefile 的配置,打开 Makefile ,在 SRC_C 中添加刚才新建的 modjs.c

SRC_C = \
	main.c \
	modjs.c \
	mphalport.c \
	modutime.c \

这样就可以了,重新编译后, js.emit("string") 就会在标准输出中显示它的参数,这说明定义一个模块的事做好了。

接着考虑和 js 的对接,以实现最终目的。

3.4. 自定义模块与 js 的对接

这一步,最终的目的是 js.emit 调用之后,就可以从一个事先定义的,确定的变量中,获取到 emit 的内容。

大概的思路:

modjs.c 中的 js_emit 里,加入一个函数,传出字符串:

extern void mp_on_emit(const char *s);

STATIC mp_obj_t js_emit(const mp_obj_t str) {
    const char* s = mp_obj_str_get_str(str);
    mp_on_emit(s);
    return mp_const_none;
}

mp_on_emitmain.c 中定义,把接收到的字符串马上复制一遍,保存在一个静态变量中:

STATIC char *mp_current_emit_str = "";
void mp_on_emit(const char *s) {
    strcpy(mp_current_emit_str, s);
}

同时在 main.c 中提供一个 mp_js_resiger_emit_handler(char *s) ,之后会被导出到 js ,它的参数是一个内存地址,实现上将会赋值给静态变量 mp_current_emit_str

void mp_js_register_emit_handler(char *s) {
    mp_current_emit_str = s;
}

因为 mp_js_resiger_emit_handler 的参数是由 js 传入的,所以,在 js_emit 调用完之后,我们就可以从 js 传入的地址那里,得到可能存在的 emit 内容。整体的 nodejs 过程如下:

var mp_js = require('./build/micropython.js');
mp_js().then(Module => {
  var mp_js_init = Module.cwrap('mp_js_init', 'null', ['number']);
  var mp_js_do_str = Module.cwrap('mp_js_do_str', 'number', ['string']);
  const buffer = Module._malloc(200 * 1024);
  Module._mp_js_register_emit_handler(buffer);
  mp_js_init(64 * 1024);
  const py = `
import js
import json
js.emit(json.dumps({"a": 123}))
  `
  mp_js_do_str(py);
  const str = Module.UTF8ToString(buffer);
  Module._free(buffer);
  console.log(str);
});

额外注意的一个点,这里用到了更多的 emcc 导出函数,同时还以 Promise 的方式使用模块,所以 Makefile 中的 FLAGS 部分也要作相应的修改:

JSFLAGS += -s EXPORTED_FUNCTIONS="['_malloc', '_free', '_mp_js_register_emit_handler', \
                                   '_mp_js_init', '_mp_js_init_repl', '_mp_js_do_str', \
                                   '_mp_js_process_char', '_mp_hal_get_interrupt_char', \
                                   '_mp_sched_keyboard_interrupt']" \
           -s EXPORTED_RUNTIME_METHODS="['ccall', 'cwrap', 'UTF8ToString']" \
           -s --memory-init-file 0 --js-library library.js -s MODULARIZE

除了加入额外导出的函数,还添加了 -s MODULARIZE ,否则无法判断模块初始化完成的时机(如果只是在 REPL 中验证 WebAssembly 的正确执行,那不会遇到这个问题)。

3.5. 实际的应用使用

前面已经处理好了 micropython.jsfireware.wasm ,现在把它们应用到实际的业务场景中。

要做的事,是对于下图这样的一个数据可视化的场景,本来有一个简单,固定的描述结构(JSON):

我们使用 Python ,替换原来固定的,静态的 JSON 结构。完整的编程语言语法,不光为结构描述带来了抽象能力,还为以后加入更完整的功能直接铺平了道路,比如数据的获取及数据获取后的再加工。

要把 Python 用于这个场景,前面的工作,已经做得差不多了,剩下的,多是处理一些环境方面的问题:

micropython.jsfireware.wasm 直接复制到项目中就可以使用。注意 micropython.js 中有 fs 相关的东西,最简单的处理方式,就是在 webpack 的配置中,加一条 node: {"fs": "empty"}

const webpackConfig = {
    mode: 'development',
    node: {"fs": "empty"},
    entry: [
        'webpack-dev-server/client?http://localhost:8080',
        ...

micropython.js 原来默认的那个 _mp_js_write() 实现,可以用 UTF8ToString 改掉,否则中文显示有问题:

function _mp_js_write(ptr, len) {
    var str = UTF8ToString(ptr, len);
    var mp_js_stdout = document.getElementById('mp_js_stdout');
    var print = new Event('print');
    print.data = str;
    mp_js_stdout.dispatchEvent(print);
}

mp_js_init ,初始化的栈大小,需要给的大点,比如 5M ,否则执行现实的功能代码,空间可能会不够。

最后执行的 Python 代码,可以在 https://gist.github.com/yszou/14e894cb20fc6f017aa077626749c70e 看到。功能上,就是实现随机生成右侧的图。过程中遇到了没有 uuid 模块,及 MicroPython 不支持 gb2312 编码的问题,我没想解决它们,绕过去就好了。

3.6. 小结

4. 相互调用背后的东西

目前,浏览器中使用 WebAssembly ,是通过一组 WebAssembly 的 API 完成的。大概的过程是:

前面说过,因为我们不需要手写 WebAssembly 语言,所以这个过程大概知道就行了。具体使用时,像 emcc ,它又会根据 C 编译结果的情况,对这一过程做自己的定制。所以,现在各种编译到 WebAssembly 的方案,产出物除了 wasm 文件之外,都会包括对应的 js 加载辅助实现。直接拿到 wasm 源文件,不知道它的导入导出依赖,也是没法用的。

5. 网络 IO 和异步

5.1. Emscripten 中的 HTTP

emcc 提供了一组 fetch API ,实现了 HTTP 请求,默认是异步行为,编译时需要添加额外的参数。

#include <stdio.h>
#include <string.h>
#include <emscripten/fetch.h>

void onSuccessed(emscripten_fetch_t *fetch) {
    printf("%llu, %s\n", fetch->numBytes, fetch->url);
    printf("%s\n", fetch->data);
    emscripten_fetch_close(fetch);
}

void onError(emscripten_fetch_t *fetch) {
    printf("%d, %s\n", fetch->status, fetch->url);
    emscripten_fetch_close(fetch);
}


int main() {
    emscripten_fetch_attr_t attr;
    emscripten_fetch_attr_init(&attr);
    strcpy(attr.requestMethod, "GET");
    attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
    attr.onsuccess = onSuccessed;
    attr.onerror = onError;
    printf("hello here\n");
    return 0;
}

使用上还是很简单直接的,最终的请求也是通过浏览器的 XHR 发出。(nodejs 不能直接运行,会报错)

编译时,需要添加额外的 FETCH 选项:

emcc -sFETCH -o demo.html demo.c

虽然 emcc 的文档中声称 FETCH 这部分有同步的支持,但实际上只是运行时环境上的一些包装。要么是使用 web workder ,要么是像实现线程机制那种,依赖 SharedArrayBuffer (浏览器因为安全原因都在去掉它了)。如果没有额外的收益,引入这些东西又会把项目搞得复杂。

5.2. MicroPython 中的情况

很遗憾,从目前我了解到的情况, MicroPython 的 js 移植无法使用异步。由 MICROPY_PY_UASYNCIO 宏控制的 asyncio 模块在 js 移植中也是没有打开的。

没有异步机制,加上 mp_js_do_str 的执行是 js 的独占,那么直接在 MicroPython 中,去调用 emcc 的异步 HTTP ,自己用 while True 模拟同步的网络请求也是不可行的,因为 mp_js_do_str 函数一直没有“返回”,这样做会阻塞浏览器页面。

针对这个问题,个人现在的一个想法是:

6. 可能的应用场景

总的来说,我觉得 WebAssembly 的直接应用场景是比较窄的,虽然像是浏览器环境的“汇编语言”,但是本身又不是必须的。所以它带来的“集成”的能力可能最重要。性能方面的例子,我自己是找不到的,V8 本身是高度优化的东西,一套基本的 WebAssembly 虚拟机,未必就比它性能好。而且浏览器 API 中还有些是直接硬件层面集成,也是高度优化的东西。

Emscripten 倒是利用 WebAssembly 提供了一个很简单的和 C 集成的方案,有 C 就等于有了一切现成的广大资源。至少那些 js 没有,或者不善于做的事,都可以快速补上,可以带来更多的想象力和可能性吧 —— 不过上限也还是浏览器,做浏览器允许做的事,而不是操作系统允许做的事。

评论
©2010-2022 zouyesheng.com All rights reserved. Powered by GitHub , txt2tags , MathJax