玩具 ruby​​.wasm ~ 直接使用 Web Worker

由于 Ruby 3.2 支持基于 WASI 的 WebAssembly,因此已经发布了预览版。

这篇文章老老实实看不懂 WebAssembly 或者 WASI1这是一篇关于人类试图在浏览器上运行 Ruby 的文章。既然感觉暂时应该可以用,我想大概有很多无用的描述吧。注意。

做什么

这就像在浏览器上运行一个用文本框或类似内容编写的任意 Ruby 脚本并获取执行结果。

简而言之RubyOnBrowser试试红宝石我想再冲一杯类似的东西。

暂时运行 Ruby 脚本

ruby.wasm 的 github以上快速入门(适用于浏览器)既然上市了,首先,这几乎就是这样。

ruby1.html

<html>
  <script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/browser.umd.js"></script>
  <script>
    const { DefaultRubyVM } = window["ruby-wasm-wasi"];
    const main = async () => {
      const response = await fetch(
        "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/ruby.wasm"
      );
      const buffer = await response.arrayBuffer();
      const module = await WebAssembly.compile(buffer);
      const { vm } = await DefaultRubyVM(module);

      alert(vm.eval(`(1..10).inject(:*)`).toString());
    };

    main();
  </script>
  <body></body>
</html>

现在(1..10).inject(:*)的计算结果显示在警报中。

启用标准库

https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/ruby.wasm在上面的HTML中读到的基本不包括标准库。2所以写require "date"会报错。如果要使用标准库,则必须加载另一个文件。

请注意,最新的(https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/) 目前不包含任何标准库文件。既然说只有nightly release版本有标准库,就在上面链接右上角的选择框中选择“ruby-head-wasm-wasi@0.3.0-(date)-a”,@987654325 @ → 使用ruby+stdlib.wasm。另外,让我们将之前 HTML 文件第二行中以<script src="~"> 加载的文件更改为具有相同日期版本的browser.umd.js

我想ruby+stdlib.wasm大概会在3.2正式版发布的时候放在最新的位置,但就目前而言,这种关注似乎是必要的。

* 在本文中,我们将暂时使用 2022/09/29 版本。

vm.eval

您给vm.eval 的字符串将作为Ruby 脚本执行。 Kernel.#eval 返回最后一个表达式的值。

返回的数据是一个不起眼的对象,但是通过.toString(),似乎可以在Ruby中得到.to_s的字符串。

由于可以通过这种方式将字符串数据从 Ruby 端发送到 JavaScript 端,例如发送 JSON 字符串,也可以发送数组和哈希数据。 (也许有更简单的方法,但我没有研究过。)

以便在文本框中输入的脚本可以执行

ruby2.html

<html>
  <script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.3.0-2022-09-29-a/dist/browser.umd.js"></script>
  <script>
    let RubyModule;
    const { DefaultRubyVM } = window["ruby-wasm-wasi"];
    const main = async () => {
      const response = await fetch(
        "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.3.0-2022-09-29-a/dist/ruby+stdlib.wasm"
      );
      const buffer = await response.arrayBuffer();
      const module = await WebAssembly.compile(buffer);
      RubyModule = module;
      document.getElementById("run").disabled = false;
    };

    main();

    async function run(){
      const script = document.getElementById("script").value;
      const result = document.getElementById("result");
      const { vm } = await DefaultRubyVM(RubyModule);
      result.value = "";
      result.value += vm.eval(script).toString();
    }
  </script>
  <body>
    <textarea >(1..10).inject(:*)</textarea>
    <button  onclick="run()" disabled>実行</button>
    <textarea  readonly></textarea>
  </body>
</html>

就目前而言,有可能是这样的。

这样每次运行时它都会重置

鉴于我们想要创建的内容的性质,最好在每次运行时都将其重置。也就是说,先在文本框中输入$a = 1并执行,然后将文本框改为$a并执行,我要nil而不是1

在第一个ruby1.html中,中间产品responsebuffermodule是在准备好最终执行Ruby脚本时需要的vm时创建的。

当我尝试它时,如果我保存vm并重用它,之前的执行结果仍然存在,但是如果我保存module并从那里生成vm,我可以使用之前的执行结果似乎被重置。上面的ruby2.html 就是这样做的。

从 Ruby 操作 JavaScript

require "js"
JS::eval("実行したい JavaScript コード")

,您可以从 Ruby 运行 JavaScript。您还可以从 Ruby 端控制浏览器,因为您还可以操作 DOM。

得到输出

事实上,Ruby 脚本中输出的内容可以在控制台中显示,但不能作为值获取。

RubyOnBrowser 和 TryRuby 通过将WasmFs 替换为writeSync 来获得输出。如果你使用这种方法,RubyOnBrowser 对应部分或者TryRuby的对应部分请参见(我不确定。)

但是,作为一种更简单的方法,您也可以通过以下方式获取输出。

vm.eval(`
require "stringio"
$stdout = $stderr = StringIO.new(+"", "w")
`);
let output;
try {
  vm.eval(script);
  output = vm.eval(`$stdout.string`).toString();
} catch(err) {
  output = err.toString();
}

简而言之,它在 Ruby 中重定向标准输出并在最后返回值。

提供输入

在输出应用程序中,您还可以给标准输入一些字符串。

vm.eval(`
require "stringio"
$stdin = StringIO.new("foo")
`);

如果你想通过对话框交互地获取输入,还有这样的方法。 (*在撰写本文时,它不适用于放置在最新位置的文件。似乎有必要使用日期较新的文件。)

vm.eval(`
require "js"
module Kernel
  def gets
    JS::eval("return prompt()").to_s + "
"
  end
end
`)

获得每个输出的价值

另外,使用上面介绍的输出获取方法,直到脚本执行完成才能获取输出,但是如果想要每次有输出都获取输出,这种方法也是可以的。

vm.eval(`
require "stringio"
$stdout = $stderr = StringIO.new(+"", "w")
require "js"
def $stdout.write(str)
  JS::eval("result.value += `" + str.gsub("\\", "\\\\").gsub("`", "\\`") + "`")
end
`)

但是,即使您使用此方法运行像puts 1; sleep 1; puts 2 这样的脚本,文本框的值也不会改变,直到脚本结束。如果你想在每次有输出时立即在屏幕上显示输出,你需要结合下面描述的使用 Web Workers 的方法。

使用 Web Workers 在后台运行

这种方法确实存在问题。 Ruby 脚本运行时页面冻结。

由于我们这次要创建的是“执行在文本框中写入的任意 Ruby 脚本等”,因此有可能会执行无限循环的 Ruby 脚本。如果这样做,页面将完全冻结,在最坏的情况下,您将不得不关闭选项卡或浏览器。

但是,如果您使用一种称为 Web Worker 的机制,则可以在后台运行 Ruby 脚本,这样即使是无限循环,页面也不会冻结。

ruby3.html

<html>
  <script>
    let worker = new Worker("worker.js");
    worker.addEventListener("message", workerEvent, false);

    function workerEvent(e) {
      if (e.data[0] == "init") {
        document.getElementById("run").disabled = false;
      } else if (e.data[0] == "output") {
        document.getElementById("result").value += e.data[1];
      }
    }

    async function run(){
      const script = document.getElementById("script").value;
      document.getElementById("result").value = "";
      worker.postMessage(["script", script]);
    }
  </script>
  <body>
    <textarea >puts (1..10).inject(:*)</textarea>
    <button  onclick="run()" disabled>実行</button>
    <textarea  readonly></textarea>
  </body>
</html>

worker.js

importScripts("https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.3.0-2022-09-29-a/dist/browser.umd.js");

let RubyModule;

const { DefaultRubyVM } = this["ruby-wasm-wasi"];
const main = async () => {
  const response = await fetch(
    "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.3.0-2022-09-29-a/dist/ruby+stdlib.wasm"
    );
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  RubyModule = module;
  self.postMessage(["init", ""]);
};

main();

self.addEventListener("message", async function(e) {
  if (e.data[0] == "script") {
    const script = e.data[1];
    const { vm } = await DefaultRubyVM(RubyModule);
    vm.eval(`
require "stringio"
$stdout = $stderr = StringIO.new(+"", "w")
    `)
    let output;
    try{
      vm.eval(script);
      output = vm.eval(`$stdout.string`).toString();
    } catch(err) {
      output = err.toString();
    }
    self.postMessage(["output", output]);
  }
}, false);

这样,将要在后台运行的脚本移动到worker.js,然后将使用<script>从HTML读取的browser.umd.js改为使用importScriptsworker.js读取。 (←我花了很多时间,因为我没有理解这部分......)

此外,由于无法从Web Worker 访问windows 对象等,因此无法使用上述“通过对话框交互获取输入”的方法,因为使用了window.prompt。

使其在本地工作

上面的ruby3.html + worker.js 工作正常,如果你把它放在某个地方的网络服务器上,或者你在本地启动一个网络服务器并从本地主机访问它,但是你放在本地的文件如果打开,它将无法工作由于安全问题。

但是,似乎有一种解决方法:3

允许停止工作

使用 Web Workers 解决了无限循环脚本导致页面冻结的问题。但是到最后,死循环还是会继续在幕后运行,很浪费CPU,在那种状态下,新的执行是不可能的。

因此,例如,您应该有一个停止按钮,或者使用Worker.terminate() 在几秒钟后终止网络工作者。

但是,如果你只是结束它,之后它将无法运行,所以你需要以terminate结束+重新生成Web Worker+注册事件侦听器。

人工制品

这是我到目前为止所做的,稍作修改。来源是Github 仓库请参见

关于 ruby​​.wasm 的信息还很少,希望对以后想要使用 ruby​​.wasm 的人有所帮助。

顺便说一句,除了前面提到的 RubyOnBrowser 和 TryRuby 之外,我发现 Python 中的 Wasm 实现很有用。皮奥德斯曾是。这个地方有更多的历史和更多的信息。 (感谢 Pyodide,我发现我实际上需要使用本文中提到的importScripts。)

可交付成果 2

对于那些想要使用 ruby​​.wasm 创建东西的人来说,上述工件可能是有用的参考,但它们并不实用。

作为一个实用的东西,我也做了这样的东西。

这是一个面向竞争激烈的 Ruby 程序员的系统,可以针对多个输入示例运行程序,并共同检查执行结果是否与输出示例匹配。如果有人在用 Ruby 进行竞争性编程,如果你能使用它,我会很高兴。 (使用 Pyodide蟒蛇版本还有)

  1. 首先,即使是 JavaScript 也不是很好理解,是否可以说 Ruby 很好理解是值得怀疑的。

  2. stringio 包括在内。我还没有检查里面还有什么。

  3. 我不知道为什么它首先受到限制,以及为什么它很容易避免时应该受到限制。

原创声明:本文系作者授权爱码网发表,未经许可,不得转载;

原文地址:https://www.likecs.com/show-308632234.html