前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >WebAssembly探索之旅

WebAssembly探索之旅

作者头像
车雄生
发布2023-07-09 14:43:06
4950
发布2023-07-09 14:43:06
举报
文章被收录于专栏:咩嗒

本文从最简单函数调用开始,逐步探索c库的调用,多文件/模块链接,WASI,函数指针参数,wasm引用js对象,c函数作为js回调等话题。所有代码均不生成胶水代码,更能体现wasm的本质,所有代码都放github上,都能在node v16.16.0上运行。

不到20行的例子

如下C代码:

代码语言:javascript
复制
int add(int a, int b) {
    return a + b;
}

编译:

代码语言:javascript
复制
emcc --no-entry -O3 adder.c -o adder.wasm -s EXPORTED_FUNCTIONS="['_add']"
  • 要求输出.wasm文件,表示不需要胶水代码
  • 不加--no-entry会报错,说找不到main函数
  • EXPORTED_FUNCTIONS是导出的函数,导出后可以在js访问

js调用代码:

代码语言:javascript
复制
const fs = require('fs');

const wasmSource = new Uint8Array(fs.readFileSync("adder.wasm"));
const wasmModule = new WebAssembly.Module(wasmSource);
const wasmInstance = new WebAssembly.Instance(wasmModule, {
    env: {
    }
});

const result = wasmInstance.exports.add(2, 40);
console.log(result);
  • 前8行是wasm加载,固定套路,这部分本文所有例子都差不多
  • 加载完毕后可以在WebAssembly.Instance.exports访问到编译时声明为导出的函数

运行:

代码语言:javascript
复制
node test_adder.js #输出42

调用c库函数

作为一个调包侠,我很自然的问到:怎么调其它库?如果是第三方库,需要自行编译成wasm或者找现成的wasm版本,如果是c/c++标准库,emscripten已经内置了支持,直接使用即可。以malloc和free为例子:

代码语言:javascript
复制
//call_malloc.c
#include <stdlib.h>

void* allocStr(int len) {
    int i;
    char* p = (char*)malloc(len + 1);
    for(i = 0; i < len; i++) {
        p[i] = 'a' + (char)i;
    }
    p[len] = '\n';
    return p;
}

void freeStr(char* p) {
    if (p) free(p);
}

调用的js代码:

代码语言:javascript
复制
//。。。省略的加载代码

const ptr = wasmInstance.exports.allocStr(6);
console.log(`str(${6}) = ${ptr}`);
wasmInstance.exports.freeStr(ptr);

输出

代码语言:javascript
复制
str(6) = 67080

怎么是个数字?不是字符串么?原来WebAssembly(1.0)只有四种类型:int32,int64,float32,float64,而指针,在wasm32用int32表达,代表的是线性内存偏移,我们在allocStr后加两行代码验证下:

代码语言:javascript
复制
const ptr = wasmInstance.exports.allocStr(6);
const heap = new Uint8Array(wasmInstance.exports.memory.buffer);
console.log(`arr[0]=${heap[ptr]}, arr[1]=${heap[ptr + 1]}`);

打印如下,符合预期,0,1号字节分别是'a','b'的ascii码:

代码语言:javascript
复制
arr[0]=97, arr[1]=98

我们可以写个函数,去memory读取内容转成js字符串,篇幅的关系就不贴了,看附带的代码仓库链接。

链接

c代码

foo.c代码

代码语言:javascript
复制
#include <stdlib.h>

extern int bar(int* array, int size);

int foo(int size) {
    int i, ret;
    int* p = (int*)malloc(size * 4);
    for(i = 0; i < size; i++) {
        p[i] = i;
    }
    ret = bar(p, size);
    free(p);
    return ret;
}

静态链接

直接emcc多个文件

代码语言:javascript
复制
emcc foo.c bar.c --no-entry  -O3 -o muti_src.wasm -s EXPORTED_FUNCTIONS="['_foo']"

也可以加-c,先编译成.o文件,然后链接在一起:

代码语言:javascript
复制
emcc foo.c -c -O3 -o foo.o 
emcc bar.c -c -O3 -o bar.o 
emcc foo.o bar.o --no-entry  -O3 -o muti_src.wasm -s EXPORTED_FUNCTIONS="['_foo']"

"动态"链接

有没可能类似动态库那样,运行时链接呢?我尝试了下:

代码语言:javascript
复制
emcc bar.c -O3 -s SIDE_MODULE=1 -o bar.wasm 
emcc foo.c --no-entry  -O3 -o foo.wasm -s EXPORTED_FUNCTIONS="['_foo']" -s WARN_ON_UNDEFINED_SYMBOLS=0

然后在js代码中

代码语言:javascript
复制
const fs = require('fs');

const fooWasmSource = new Uint8Array(fs.readFileSync("foo.wasm"));
const fooWasmModule = new WebAssembly.Module(fooWasmSource);
const fooWasmInstance = new WebAssembly.Instance(fooWasmModule, {
    env: {
        bar: function() {
            return barWasmInstance.exports.bar.apply(null, arguments);
        }
    }
});

const barWasmSource = new Uint8Array(fs.readFileSync("bar.wasm"));
const barWasmModule = new WebAssembly.Module(barWasmSource);
const barWasmInstance = new WebAssembly.Instance(barWasmModule, {env: {memory: fooWasmInstance.exports.memory}});

const result = fooWasmInstance.exports.foo(100);
console.log(result);

碰到的难点是:SIDE_MODULE模块的内存由外部传入,为了实现和主模块用同一块内存,所以需要传主模块的memory,但主模块的实例化又依赖于bar模块。这问题通过一个封装的bar函数解决。

应该还有别的方式解决,比如两个文件都以SIDE_MODULE编译,统一使用一个外部构造的Memory,但会带来新问题:SIDE_MODULE似乎不会把c/c++库链接进来,运行会报找不到malloc方法。

WASI

Docker创始人Solomon Hykes说:如果2008年的时候,WASM和 WASI这两个东西已经存在了的话,他就没有必要创立 Docker了。

这WASI是何方神圣?让我们一探究竟。

如下wasi_copy.c代码

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

int main(int argc, char **argv) {
    ssize_t n, m;
    char buf[BUFSIZ];

    if (argc != 3) {
        fprintf(stderr, "usage: %s <from> <to>\n, argc expect 3, got %d", argv[0], argc);
        exit(1);
    }

    int in = open(argv[1], O_RDONLY);
    if (in < 0) {
        fprintf(stderr, "error opening input %s: %s\n", argv[1], strerror(errno));
        exit(1);
    }

    int out = open(argv[2], O_WRONLY | O_CREAT, 0660);
    if (out < 0) {
        fprintf(stderr, "error opening output %s: %s\n", argv[2], strerror(errno));
        exit(1);
    }

    while ((n = read(in, buf, BUFSIZ)) > 0) {
        char *ptr = buf;
        while (n > 0) {
            m = write(out, ptr, (size_t)n);
            if (m < 0) {
                fprintf(stderr, "write error: %s\n", strerror(errno));
                exit(1);
            }
            n -= m;
            ptr += m;
        }
    }

    if (n < 0) {
        fprintf(stderr, "read error: %s\n", strerror(errno));
        exit(1);
    }

    return EXIT_SUCCESS;
}

代码里有文件的读写,参数的获取,这也能跑在wasm上?

上面代码用emcc能编译,也能加载,但执行会报文件读取没有权限,我还以为是我代码的问题,各种查阅资料,问ChatGPT也没解决。后面我换了一个编译工具就跑成功了。到这里 下载wasi-sdk并按照其首页介绍方式编译成wasi_copy.wasm。

在js中加载wasm。

代码语言:javascript
复制
const wasi  = require( 'wasi');
const fs = require('fs');
const process = require('process');

const wasiInstance = new wasi.WASI({
  args: process.argv.slice(1),
  env: process.env,
  preopens: {
    '/sandbox': '.'
  }
});

const imports = {
    wasi_snapshot_preview1: wasiInstance.wasiImport
};

const wasmSource = new Uint8Array(fs.readFileSync("wasi_copy.wasm"));
const wasmModule = new WebAssembly.Module(wasmSource);
const wasmInstance = new WebAssembly.Instance(wasmModule, imports);

wasiInstance.start(wasmInstance);

然后执行如下命令:

代码语言:javascript
复制
node --experimental-wasi-unstable-preview1 test_wasi_copy.js /sandbox/buid.sh /sandbox/buid.sh.bak

即可把当前目录的buid.sh拷贝到buid.sh.bak。

代码说明:

  • 在js里得new一个wasi实例,该实例需要设置命令行参数以及沙箱环境,wasiInstance.start会调用到main函数
  • 初始化wasm需要导入wasi实例的一些api,如果你打印下这些api都有些啥,可以看到是一些文件读写,socket,随机数的支持。
  • node执行需要输入--experimental-wasi-unstable-preview1,而且只能读写沙箱里头的内容。

js函数作为c函数指针参数

函数指针调用在wasm翻译成call_indirect指令,指针会翻译为table的索引,这个table会被导出到__indirect_function_table,我们只要往这个table插入函数,然后调用时,用索引作为函数指针参数就可以了。。吗?试试调用如下c代码:

代码语言:javascript
复制
#include <stdlib.h>

typedef int (*compare_t)(const void *, const void*);

void sort_int_array(int *array, size_t len, compare_t compar) {
    qsort(array, len, sizeof(int), compar);
}

编译需加ALLOW_TABLE_GROWTH=1,这可以让我们在外部添加table条目

代码语言:javascript
复制
emcc --no-entry -O3 call_qsort.c -o call_qsort.wasm -s EXPORTED_FUNCTIONS="['_sort_int_array']" -s ALLOW_TABLE_GROWTH=1

js调用代码如下:

代码语言:javascript
复制
//。。。省略的加载代码

function call_qsort(arr) {
    const savedStack = wasmInstance.exports.stackSave();
    const ptr = wasmInstance.exports.stackAlloc(arr.length * 4);
    const start = ptr >> 2;
    const heap = new Uint32Array(wasmInstance.exports.memory.buffer);
    
    
    for (let i = 0; i < arr.length; ++i) {
        heap[start + i] = arr[i];
    }
    
    function cmp(pa, pb) {
        return heap[pa >> 2] - heap[pb >> 2];
    }
    
    let cmpIndex = wasmInstance.exports.__indirect_function_table.grow(1);
    wasmInstance.exports.__indirect_function_table.set(cmpIndex, cmp);

    wasmInstance.exports.sort_int_array(ptr, arr.length, cmpIndex);

    const result = [];
    for (let i = 0; i < arr.length; ++i) {
        result.push(heap[start + i]);
    }
    
    wasmInstance.exports.stackRestore(savedStack);
    return result;
}

const numbers = [14, 3, 7, 42];
console.log(numbers, 'becomes', call_qsort(numbers));

上述代码会报错,经过一番周折才得知__indirect_function_table只能插入wasm方法,最后在emcc的胶水代码中找到解决方案:通过一个动态生成的wasm,把js方法先import,然后再export出来即可,最终完整代码见附带的代码仓库链接。

wasm引用js对象(externref)

WebAssembly 1.0只有数字类型,而1.1加入了引用类型:externref和funcref,externref可以用于引用wasm之外的对象,比如js。funcref的例子我们放到下一小节讲。emscripten对externref有一定的支持:在c/c++可以有限使用(比如接下来的例子),不过我尝试定义一个externref全局变量,或者stl容器,会编译失败。 一个能编译的externref c例子如下:

代码语言:javascript
复制
typedef char __attribute__((address_space(10)))* externref;

externref pass_externref(externref p) {
    return p;
}

编译需要加-mreference-types参数:

代码语言:javascript
复制
emcc --no-entry -O3 pass_externref.c -o pass_externref.wasm -s EXPORTED_FUNCTIONS="['_pass_externref']" -mreference-types

调用pass_externref的nodejs代码:

代码语言:javascript
复制
const input = { message: 'Hello, WebAssembly!' };
const output = wasmInstance.exports.pass_externref(input);
console.log('Input:', input);
console.log('Output:', output);

nodejs运行上述脚本需要加--experimental-wasm-reftypes参数:

代码语言:javascript
复制
node --experimental-wasm-reftypes test_pass_externref.js

输出结果:

代码语言:javascript
复制
Input: { message: 'Hello, WebAssembly!' } 
Output: { message: 'Hello, WebAssembly!' }

c函数作为setTimeout回调

这里需要用到另一个引用类型:funcref。这个例子是我无意中找到的,出处在这里 。这例子实现了一个c函数作为回调调用setTimeout。

被调用的funcref_example.c:

代码语言:javascript
复制
typedef char __attribute__((address_space(20)))* funcref;
typedef void* funcref_ptr;

// imported from javascript
extern int setTimeout(funcref callback, int timeout);
extern funcref funcptr_to_funcref(funcref_ptr);
extern void printSomeInfo();

void some_proc() {
    printSomeInfo();
}

void setTimeoutByCFunc(int i) {
    setTimeout(funcptr_to_funcref((funcref_ptr)&some_proc), i);
}

funcptr_to_funcref通过汇编文件funcref_example.support.S实现:

代码语言:javascript
复制
.globl __indirect_function_table
.tabletype __indirect_function_table, funcref

.globl funcptr_to_funcref

funcptr_to_funcref:
    .functype funcptr_to_funcref(i32) -> (funcref)
        local.get 0
        table.get __indirect_function_table
    end_function

编译:

代码语言:javascript
复制
emcc --no-entry -O3 funcref_example.c funcref_example.support.S -o funcref_example.wasm -s EXPORTED_FUNCTIONS="['_setTimeoutByCFunc']" -mreference-types -s WARN_ON_UNDEFINED_SYMBOLS=0

nodejs调用代码:

代码语言:javascript
复制
//。。。省略的加载代码

const wasmInstance = new WebAssembly.Instance(wasmModule, {
    env: {
        printSomeInfo: function() {
            console.log('printSomeInfo')
        },
        setTimeout: setTimeout
    }
});

wasmInstance.exports.setTimeoutByCFunc(2000);

同样,得加--experimental-wasm-reftypes参数:

代码语言:javascript
复制
node --experimental-wasm-reftypes test_funcref_example.js

运行2秒后打印printSomeInfo

完整代码仓库

wasm_demo

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-06-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 不到20行的例子
  • 调用c库函数
  • 链接
    • c代码
      • 静态链接
        • "动态"链接
        • WASI
        • js函数作为c函数指针参数
        • wasm引用js对象(externref)
        • c函数作为setTimeout回调
        • 完整代码仓库
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档