编译C到WebAssembly(WASM)并在浏览器中运行
#c #webassembly #llvm #emscripten

介绍

将C汇编为WASM的流行工具链是emscripten。它为标准C库和其他接口(例如SDL2)提供绑定,并允许将C代码编译为WASM并在浏览器中运行。

不幸的是,emscripten很复杂。它在HTML和JavaScript文件中生成了许多JavaScript代码样板。

如果您只需要编译不使用标准C库或其他接口的C代码,则可以使用更简单的工具链-clangllc

下面的示例演示了如何将C代码编译到WASM,在浏览器中运行并使用标准C类型(包括指示)与JavaScript进行通信。

先决条件

在Mac上,仅需要从brew安装llvm才能使clang编译器生成WASM代码:

brew install llvm

然后,必须将llc工具链的路径添加到PATH环境中:

export PATH=/opt/homebrew/opt/llvm/bin:$PATH

之后,clang编译器应支持--target=wasm32目标:

llc --version | grep wasm

编译C到WASM

main.c

unsigned char mem[0x10000];

const char *const upper(char *const str, const int sz)
{
    for (int i = 0; i < sz; i++)
        if (str[i] >= 'a' && str[i] <= 'z')
            str[i] -= 0x20;
    return str;
}

将其编译到WASM:

clang \
--target=wasm32 \
--no-standard-libraries \
-Wl,--export-all -Wl,--no-entry \
-o main.wasm \
main.c

该命令创建一个文件main.wasm,即二进制WASM模块。

这个微不足道的示例演示了简单的C到 - - 瓦斯接口的基本需求:

  • 使用预先分配的静态内存缓冲区(例如,大小为64KB)
  • 可以从JavaScript和通过原始类型(整数,指针等)调用C函数的能力
  • 从C函数返回原始类型的能力
  • 观察内存缓冲区从JavaScript变化的能力

标准C库未链接到WASM模块,因此C函数不应使用任何标准C函数。

有趣的是,函数upper()的结果(是指指针)作为数字返回到JavaScript,而不是指针。该数字是javaScript侧的WebAssembly内存缓冲区开头的偏移。

c处理指针,但JavaScript将内存缓冲区视为平坦的数组,并使用偏移访问其元素。

实际上,WASM“直接内存访问”并不是它的含义。 WASM内存是一个常规的JavaScript对象,它是字节的平坦数组。它的生命周期由JavaScript垃圾收集器管理为任何其他JavaScript对象。

在浏览器中运行WASM

使用以下main.html文件在浏览器中运行WASM模块:

<!DOCTYPE html>

<html>
    <body>
        <script type="module">
            const log = console.log;
            console.log = (...x) => {
                document.body.innerHTML += x.join(" ") + "<br>";
                log(...x);
            };
            const wa = await WebAssembly.instantiateStreaming(
                fetch("main.wasm"),
                { js: { mem: new WebAssembly.Memory({ initial: 0 }) } }
            );
            const memPtr = wa.instance.exports.mem.value;
            console.log("memPtr", memPtr);

            const mem = new Uint8Array(wa.instance.exports.memory.buffer);

            const str = "Hello, world!";
            new TextEncoder().encodeInto(
                str,
                mem.subarray(memPtr, memPtr + str.length)
            );
            console.log(
                "upper()",
                wa.instance.exports.upper(memPtr, str.length)
            );
            console.log(
                new TextDecoder().decode(
                    mem.subarray(memPtr, memPtr + str.length)
                )
            );
        </script>
    </body>
</html>

main.htmlmain.wasm文件需要由Web服务器提供。

例如,由Python的SimpleHttpserver:

python -m SimpleHTTPServer

或VSCODE的LIVE服务器扩展。

您应该在浏览器和DevTool控制台中看到类似的东西:

memPtr 1024
upper() 1024
HELLO, WORLD!

1024实际上是“指针”。 1024是WASM模块内存中mem C数组的开始的偏移。

upper()函数转换“你好,世界!”到上案,并将相同的“指针” 1024返回到javascript。

就是这样。 WASM模块已编译并在浏览器中运行。