SqueakJS 是一个纯 JavaScript 实现的 Squeak 虚拟机, 能够运行未经修改的 Smalltalk image。 – SqueakJS 主页

前言

当我们说 “在 Squeak 中编程”, 通常指的是在 Squeak image 中编程, Squeak image 运行在 Squeak 虚拟机之上。 之前的 Squeak 虚拟机从 C 代码中生成, 在不同的操作系统上编译为不同的虚拟机。 这些虚拟机运行相同的 Squeak image, 用户编程发生在 image 中, 这使得不同的操作系统下拥有相同的 Squeak 编程体验。

SqueakJS 是使用 JavaScript 实现 Squeak 虚拟机, 这使得我们在 Linux/MacOS/Windows 之外, 又多了一个可以运行 Squeak image 的平台: 浏览器平台, 我喜欢将浏览器看作新的操作系统

由于 SqueakJS 运行的 image 与其他 Squeak 虚拟机完全相同, 因此我们在 Squeak 学习笔记 中积累的经验全都可以用在 SqueakJS 里。

SqueakJS 提供了与 JavaScript 互操作的 API, 这样一来, Squeak 就拥有与 JavaScript 一样的能力。 在每年巨额资金的投入下, 现代浏览器拥有极其丰富的 API, 而且还在随着时间不断增强。 现在, SqueakJS 免费获得了所有这些能力!

Squeak 生态的一个问题是社区很小, 第三方库不够丰富, 尽管拥有极其强大的内核和开发环境, 但许多工作都得从头开始, 这使得 Squeak 的开发效率并不一定比其他语言更高, 因为在一门主流语言中, 使用一个第三方库可能就解决了问题的大半。

SqueakJS 使我们可以兼得鱼与熊掌:

  • 既可以使用浏览器生态庞大的设施
  • 又可以拥有 Squeak 的所有好处 (这些好处里我最珍视可理解性, 我可以将好奇心的触角伸到系统的每个角落)

SqueakJS 还带来了其他好处:

  • 更好的可理解性。由于 SqueakJS 采用 JavaScript 这样的高级语言编写, 因此阅读起来要比 C 的实现好读很多(Slang 可能抵消了一部分好处)。
  • 更好的可扩展性。由于浏览器出色的的跨平台性, 要想扩展 SqueakJS , 只需编写一次 primitive, 不用像之前得为不同的操作系统分别编写。 而且 primitive 是使用高级语言(JavaScript)编写的, 写起来要比之前容易。随着 WebAssembly 的成熟, 未来还有更广阔的选择。
  • 支持移动端。为移动端编译和发布 Squeak 是繁琐的, 在 iOS 麻烦尤其多。运行在浏览器中的 SqueakJS 使得这些问题变得简单, PWA 使其能够如原生程序那样运行。 浏览器内置了多点触控的 api, 这为 Morphic 开启了新的交互可能。

使用笔记

官方部署了一个启动器(Launcher)。旅程从选择一个 image 开始。

Mini Squeak

Mini Squeak 是早期的 Squeak image, 非常精简, 只有 624KB, 是目前 SqueakJS 支持的最小 image。

其 UI 风格类似 Smalltalk 80, 基于 MVC 构建。

Mini Squeak 是 Squeak, 因此在其中编写的代码, 也可直接运行在现代 Squeak 上。

Mini Squeak 默认加载了 JSBridge, JSBridge 允许 Squeak 与 JavaScript 直接的互操作以下是官方的一些实例:

1
2
3
4
5
"Call global function"
JS alert: 'Squeak says Hello World!'.

"Call function on global object (open console to see result)"
JS console log: 'Squeak says Hello World!'.

JSBridge

JSBridge 可以与 DOM 交互、访问 JavaScript 库以及使用 Smalltalk 代码创建交互式 HTML 界面。

Mini Squeak 默认加载 JSBridge, 其他的 squeak image 没有默认加载它, 但你也可以使用。

在 FileList 找到它, 然后 filein 即可:

JSBridge 支持的最新 image 为 Squeak 5. Squeak 6 移除了许多旧的类, 与 JSBridge 存在兼容性问题。

我目前比较偏好在 Squeak4.5 中使用它(Squeak 5 有点慢), Squeak4.5 image 只有 15.3 MB。

Craig Latta 在 Caffeine 中对 JSBridge 做了增强

Caffeine 有一个名为 JSObject 的附加代理类, 它提供附加的反射功能, 例如枚举主体 JS 对象的属性。 –Caffeine :: Livecode the Web!

持久化

所有 image 和相关文件都持久存储在浏览器内的数据库(IndexedDB)中。

用户可以在两处上传/下载这些文件:

  1. Squeak image: FileList
  2. 浏览器: 启动器(Launcher)
    • 本地版本的启动器: http://localhost:3000/run/

有了这两个工具, 我们就可以在 SqueakJS 与本机 Squeak 之间交换文件, 可以做的事情包括:

  • fileout 本机的 package(如 Dynatalk), 通过 Launcher 上传, 然后在 SqueakJS 的 image 中 filein.
  • 保存 SqueakJS 中的 image, 从 Launcher 下载到本地之后, 使用本机 Squeak 运行它. (参考 Suspend in the browser, resume on the desktop.)
  • 使用自己的 image: SqueakJS 只是一个虚拟机, 因此可以使用它加载自己的 Squeak image, 只需上传到 Launcher 即可打开

有了持久化之后, SqueakJS 就可以提供接近本地应用的体验。 人们喜欢能够保存正在进行的工作, 不希望它是一个每次都从头开始的"干净"网络应用。

开发笔记

本地开发

SqueakJS 是一个"纯前端项目", 要在本地运行它是很容易的:

1
2
3
4
5
git clone https://github.com/codefrau/SqueakJS.git
cd SqueakJS
# npx serve 运行一个本地 http 服务器, 我的本地 npx 有问题, 使用 Python 替代
# npx serve
python -m http.server 3000

本地启动器(Launcher): http://localhost:3000/run/

参考 Installing locally

在 lively 中开发

我(Vanessa Freudenberg)正在使用 “Lively” 开发 SqueakJS, 这是一个基于浏览器的 JavaScript 开发环境,受 Smalltalk 启发。我不必在每次源代码更改后重新加载页面,而是在我的 Lively SqueakJS 调试器中执行 Squeak 并在其运行时更改其代码。这就是为什么 SqueakJS 源代码具有一种略显不寻常的布局,它符合 Lively 的开发方式。如果你觉得更简单,你仍然可以使用纯文本编辑器。

SqueakJS 是一个"纯前端项目", 但却是一个操作系统级别的 “纯前端项目”。Smalltalk 在传统上充当自身的操作系统。

要编写 SqueakJS 这样复杂的项目, 没有一个强大的开发环境, 是令人畏惧的。

Lively 提供了类似 Smalltalk 的开发体验(“将手伸到计算机中”), 拥有出色的 “liveness”。

SqueakJS 有一个 在线的 lively 开发环境

你也可以本地运行lively, SqueakJS 提供了一份在 lively 中开发的教程, 但存在 2 个问题:

  1. start.sh 无法运行, 原因是 docker 脚本年久失修
  2. squeak.html 有个 bug

要将 SqueakJS 的 lively 环境跑起来, 需要:

  1. 在本机环境下将 lively 跑起来(我在nodejs v20.11.0上成功运行了)
  2. 将 SqueakJS 代码目录软链接到 lively 的 user 目录下: ln -s xxx/SqueakJS xxx/LivelyKernel/users/SqueakJS
  3. squeak.html 中的 Global.Squeak.stopAudio() 改为 Global.Squeak.stopAudioIn()
  4. 访问 http://localhost:9001/users/SqueakJS/lively/squeak.html

Reset

目前存在一个疑问, 修改了虚拟机代码(如vm.plugins.javascript.js)之后, 需要点击 Reset 按钮才会生效, 不清楚为何不是立刻生效(常见的lively 对象都是修改完方法立刻生效)。

Reset 按钮会做的事情:

jsbridge

在 lively 中从服务器加载 mini image, 其中的 jsbridge 包基本是空的(从 browser 和 FileList 可以看出), 但不清楚为何 JS alert: 'Squeak says Hello World!'. 可以正常运行。

调试时, 我一般会上传 JSBridge.st, 然后在 FileList filein.

jsbridge 的潜在用途

前边提到:

现代浏览器拥有极其丰富的 API

我对以下 API 尤其感兴趣:

  • Web Bluetooth
  • Web Serial
  • WebGPU
  • Web Workers
  • Media Capture and Streams(camera…)
  • Touch Events
  • Sensor(Accelerometer, Gyroscope)
  • WebAssembly
  • File System API
  • WebSockets
  • WebXR

通过 jsbridge 我们能够将这些强大的能力带入 Squeak!

JavaScript 生态中有大量有趣的第三方库, 通过 jsbridge , 我们也可以将其带入 Squeak

关于 jsbridge 在 web 中各种潜在用途, Craig Latta 在博客里做了很多精彩分享。

pyodide

浏览器生态中还有其他有趣的项目, 如 pyodide(运行在浏览器中的 Python)。 我有丰富的 Python 经验, 将其舍弃难免可惜, 尤其是考虑到 Python 庞大的生态, pyodide 将这些资源全部带到了浏览器生态里, 并提供了 JavaScript 接口。 这使得我们也可以通过 jsbridge 使用它们。

Dynatalk

我近期打算将 dynatalk-squeak 移植到 SqueakJS 中, 思路如下: 重写 MQTTSpace, 使其基于 MQTT.js, 通过 jsbridge 我们可以使用 MQTT.js.

我打算将本地的 dynatalk 包 fileout 出来, 然后 filein 到 Squeak4.5 里开发它。

添加依赖库: MQTT.js

参考 jsbridge demo 里的做法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"Load MQTT.js"

| script |

(JS at: #mqtt) ifNil: [
	script := JS document createElement: 'SCRIPT'.
	script at: 'src' put: 'https://unpkg.com/mqtt/dist/mqtt.min.js'.
	script at: 'type' put: 'text/javascript'.
	((JS document getElementsByTagName: 'head') at: 0) appendChild: script.
].

使用 MQTT.js

发布消息

1
2
3
4
5
6
|client|

"client := JS mqtt connect: 'wss://guest:test@mqtt.aimaker.space:8084/mqtt'."
client := JS mqtt connect: 'ws://guest:test@127.0.0.1:15675'.
"JS console log: client."
client publish:'squeakjs' and:'Hello mqtt'.

在另一个 MQTT 客户端(MQTTX)成功收到.

订阅消息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"client on:'connect' and: []."

"+: match all"
client subscribe: '+'.
 
client on:'message' and: [:topic :message |
  "message is Buffer"
  "Beeper beep."
  JS console log: message toString.
].

FAQ

SqueakJS 与 lively 的区别是什么?

SqueakJS 基于 lively 开发。它们与 Smalltalk 有许多联系, 通过它们与Smalltalk的关系, 可以看到它们的区别与联系:

  • SqueakJS 是一个纯 JavaScript 实现的 Squeak 虚拟机, 理论上也可以使用 WebAssembly 实现
  • lively 试图成为一个自支持 web 开发环境, 拥有 Smalltalk 那样的开发体验, 但它并不是 Smalltalk, 它只是改造后的 JavaScript。

SqueakJS 的效率如何?

Make it work, make it correct, make it fast, make it cheap –Alan Kay

我在以下连下两个环境中测试它的性能:

我的机器配置是:

  • Apple M1 / 16 GB
  • MacOS 14.3.1

我使用这里提到的测试方法: 0 tinyBenchmarks, 以下是测试结果:

  • 浏览器: 350,000,000 bytecodes/sec; 2,200,000 sends/sec
  • 本机: 4,000,000,000 bytecodes/sec; 270,000,000 sends/sec

测量结果包括了:

  • 每秒发送的字节数(bytecodes/sec): 4,000,000,000 / 350,000,000 = 11
  • 每秒发送的消息数量(sends/sec): 270,000,000/ 2,200,000 = 122

如何为 SqueakJS 编写插件

SqueakJS 插件为 vm 提供新的 primitives.

参考 ExamplePlugin.js

SqueakJS 有很多旧的插件是使用 vmmaker (Slang)自动生成的, 诸如 BitBltPlugin.js

为 SqueakJS 编写插件非常容易。 由于 jsbridge 拥有与 JavaScript 双向调用的能力, 使用 jsbridge 似乎更简单。

参考