跳到主要内容

1 篇博文 含有标签「Wasm」

查看所有标签

在 JavaScript 中使用 MoonBit 的高性能 Wasm 库

· 阅读需 6 分钟

封面

在我们之前的一篇博客文章中,我们已经开始探索如何在 MoonBit 的 Wasm GC 后端中直接使用 JavaScript 字符串。正如我们所见,不仅可以用 MoonBit 编写一个兼容 JavaScript 的字符串操作 API,而且在编译为 Wasm 后,生成的产物体积非常小。

然而,您可能会好奇,在更现实的使用场景中,这将会是什么样子。因此,今天我们展示一个更贴近实际的场景:在一个由 JavaScript 驱动的 Web 应用程序中渲染 Markdown 文档,并借助 MoonBit 库 Cmark 和 Wasm 的 JS 字符串内置提案。

动机

Cmark 是一个新的 MoonBit 库,用于处理 Markdown 文档,可以解析纯 CommonMark 和各种常见的 Markdown 语法扩展(如任务列表、脚注、表格等)。此外,它从早期开始就支持外部渲染器,并附带了一个名为 cmark_html 的官方 HTML 渲染器实现。

鉴于 Markdown 在当今网络世界中的广泛存在,将 Markdown 转换为 HTML 的流程仍然是几乎每个 JavaScript 开发者工具箱中的重要工具。因此,这也成为展示 MoonBit Wasm GC API 在前端 JavaScript 中使用的理想场景。

封装 Cmark

为了进行这个演示,先创建一个新的项目目录:

> mkdir cmark-frontend-example

在该目录中,首先创建一个 MoonBit 库 cmarkwrap 来封装 Cmark

> cd cmark-frontend-example && moon new cmarkwrap

这个额外的项目 cmarkwrap 的主要作用是:

  • Cmark 本身不通过 FFI 边界暴露任何 API,这对大多数 MoonBit 库来说是常见的情况;
  • 我们需要从 mooncakes.io 仓库中获取 Cmark 项目,并将其本地编译为 Wasm GC。

cmarkwrap 的结构非常简单:

  • cmark-frontend-example/cmarkwrap/src/lib/moon.pkg.json:

    {
    "import": ["rami3l/cmark/cmark_html"],
    "link": {
    "wasm-gc": {
    "exports": ["render", "result_unwrap", "result_is_ok"],
    "use-js-builtin-string": true
    }
    }
    }

    这个配置基本与之前的博客中介绍的设置相同,为 Wasm GC 目标启用了 use-js-builtin-string 标志,并导出了相关的封装函数。

  • cmark-frontend-example/cmarkwrap/src/lib/wrap.mbt:

    ///|
    typealias RenderResult = Result[String, Error]

    ///|
    pub fn render(md : String) -> RenderResult {
    @cmark_html.render?(md)
    }

    ///|
    pub fn result_unwrap(res : RenderResult) -> String {
    match res {
    Ok(s) => s
    Err(_) => ""
    }
    }

    ///|
    pub fn result_is_ok(res : RenderResult) -> Bool {
    res.is_ok()
    }

    这里是关键部分。render() 函数封装了底层的 @cmark_html.render() 函数,不再是一个抛出异常的函数,而是返回一个 RenderResult 类型。

    然而,由于 RenderResult 是一个 Wasm 对象(而不是数字或字符串),对 JavaScript 来说是不透明的,因此无法直接被 JavaScript 调用者使用。因此,我们还需要在 MoonBit 中提供拆解该类型的方法:result_unwrap()result_is_ok() 函数正是为此目的设计的,它们接受一个 RenderResult 输入。

与 JavaScript 集成

现在是编写项目 Web 部分的时候了。在此阶段,您可以自由选择任何框架或打包工具。本次演示选择在 cmark-frontend-example 目录下初始化一个最小的项目结构,无需额外的运行时依赖。以下是项目的 HTML 和 JS 部分:

  • cmark-frontend-example/index.html:

    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Cmark.mbt + JS</title>
    </head>
    <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
    <link rel="stylesheet" href="/src/style.css" />
    </body>
    </html>

    这个简单的 HTML 文件包含一个 id="app"div,稍后会用作渲染 Markdown 文档的目标。

  • cmark-frontend-example/src/main.js:

    const cmarkwrapWASM = await WebAssembly.instantiateStreaming(
    fetch("../cmarkwrap/target/wasm-gc/release/build/lib/lib.wasm"),
    {}
    {
    builtins: ["js-string"],
    importedStringConstants: "_",
    },
    );
    const { render, result_is_ok, result_unwrap } =
    cmarkwrapWASM.instance.exports;

    function cmarkWASM(md) {
    const res = render(md);
    if (!result_is_ok(res)) {
    throw new Error("cmarkWASM failed to render");
    }
    return result_unwrap(res);
    }

    async function docHTML() {
    const doc = await fetch("../public/tour.md");
    const docText = await doc.text();
    return cmarkWASM(docText);
    }

    document.getElementById("app").innerHTML = await docHTML();

    cmarkwrap 集成到 JavaScript 中相对简单。在 fetch 并加载 Wasm 产物后,可以直接调用封装函数。result_is_ok() 帮助我们判断是否在正常路径上:如果是,我们可以通过 result_unwrap() 解包跨 FFI 边界的 HTML 结果;否则,可以抛出一个 JavaScript 错误。如果一切顺利,我们最终可以将渲染结果填充到 <div id="app"></div> 中。

现在我们可以编译 MoonBit 的 Wasm GC 产物并启动开发服务器:

> moon -C cmarkwrap build --release --target=wasm-gc
> python3 -m http.server

大功告成!您现在可以在浏览器中访问 http://localhost:8000,并查看由 Cmark MoonBit 库渲染的 A Tour of MoonBit for Beginners

演示

您可以在 GitHub 上找到该演示的代码。

了解更多