由于 Ruby 3.2 支持基于 WASI 的 WebAssembly,因此已经发布了预览版。
这篇文章老老实实看不懂 WebAssembly 或者 WASI1这是一篇关于人类试图在浏览器上运行 Ruby 的文章。既然感觉暂时应该可以用,我想大概有很多无用的描述吧。注意。
做什么
这就像在浏览器上运行一个用文本框或类似内容编写的任意 Ruby 脚本并获取执行结果。
简而言之RubyOnBrowser和试试红宝石我想再冲一杯类似的东西。
暂时运行 Ruby 脚本
ruby.wasm 的 github以上快速入门(适用于浏览器)既然上市了,首先,这几乎就是这样。
<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 字符串,也可以发送数组和哈希数据。 (也许有更简单的方法,但我没有研究过。)
以便在文本框中输入的脚本可以执行
<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 id="script">(1..10).inject(:*)</textarea>
<button id="run" onclick="run()" disabled>実行</button>
<textarea id="result" readonly></textarea>
</body>
</html>
就目前而言,有可能是这样的。
这样每次运行时它都会重置
鉴于我们想要创建的内容的性质,最好在每次运行时都将其重置。也就是说,先在文本框中输入$a = 1
并执行,然后将文本框改为$a
并执行,我要nil
而不是1
。
在第一个ruby1.html
中,中间产品response
、buffer
、module
是在准备好最终执行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 脚本,这样即使是无限循环,页面也不会冻结。
<html>
<script>
let worker = new Worker("worker.js");
worker.addEventListener("message", workerEvent, false);
function workerEvent(e) {
if (e.data[] == "init") {
document.getElementById("run").disabled = false;
} else if (e.data[] == "output") {
document.getElementById("result").value += e.data[];
}
}
async function run(){
const script = document.getElementById("script").value;
document.getElementById("result").value = "";
worker.postMessage(["script", script]);
}
</script>
<body>
<textarea id="script">puts (1..10).inject(:*)</textarea>
<button id="run" onclick="run()" disabled>実行</button>
<textarea id="result" readonly></textarea>
</body>
</html>
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[] == "script") {
const script = e.data[];
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
改为使用importScripts
从worker.js
读取。 (←我花了很多时间,因为我没有理解这部分......)
此外,由于无法从Web Worker 访问windows 对象等,因此无法使用上述“通过对话框交互获取输入”的方法,因为使用了window.prompt。
使其在本地工作
上面的ruby3.html
+ worker.js
工作正常,如果你把它放在某个地方的网络服务器上,或者你在本地启动一个网络服务器并从本地主机访问它,但是你放在本地的文件如果打开,它将无法工作由于安全问题。
但是,似乎有一种解决方法:3
允许停止工作
使用 Web Workers 解决了无限循环脚本导致页面冻结的问题。但是到最后,死循环还是会继续在幕后运行,很浪费CPU,在那种状态下,新的执行是不可能的。
因此,例如,您应该有一个停止按钮,或者使用Worker.terminate()
在几秒钟后终止网络工作者。
但是,如果你只是结束它,之后它将无法运行,所以你需要以terminate
结束+重新生成Web Worker+注册事件侦听器。
人工制品
这是我到目前为止所做的,稍作修改。来源是Github 仓库请参见
-
ruby.wasm 使用示例(没有 Web Worker)
- 不使用 Web Workers 的版本。您可以使用上面的“使用对话框以交互方式获取输入”方法从
gets
的提示中获取输入。
- 不使用 Web Workers 的版本。您可以使用上面的“使用对话框以交互方式获取输入”方法从
-
ruby.wasm 使用示例(使用 Web Worker)
- 带有 Web Worker 的版本。确保每次输出时都反映输出的 Textarea,并且当您按下停止按钮或经过指定的秒数后它会自动停止。
关于 ruby.wasm 的信息还很少,希望对以后想要使用 ruby.wasm 的人有所帮助。
顺便说一句,除了前面提到的 RubyOnBrowser 和 TryRuby 之外,我发现 Python 中的 Wasm 实现很有用。皮奥德斯曾是。这个地方有更多的历史和更多的信息。 (感谢 Pyodide,我发现我实际上需要使用本文中提到的importScripts
。)
可交付成果 2
对于那些想要使用 ruby.wasm 创建东西的人来说,上述工件可能是有用的参考,但它们并不实用。
作为一个实用的东西,我也做了这样的东西。
- 批量代码测试
这是一个面向竞争激烈的 Ruby 程序员的系统,可以针对多个输入示例运行程序,并共同检查执行结果是否与输出示例匹配。如果有人在用 Ruby 进行竞争性编程,如果你能使用它,我会很高兴。 (使用 Pyodide蟒蛇版本还有)
-
首先,即使是 JavaScript 也不是很好理解,是否可以说 Ruby 很好理解是值得怀疑的。↩
-
stringio
包括在内。我还没有检查里面还有什么。↩ -
我不知道为什么它首先受到限制,以及为什么它很容易避免时应该受到限制。↩
原创声明:本文系作者授权九品源码发表,未经许可,不得转载;
原文地址:https://www.19jp.com/show-308632234.html