在线编译器原理:如何在浏览器中运行代码

浏览器端编译器直接在网页内部运行您的代码,依靠的是 WebAssembly:一种紧凑、沙箱化的二进制格式,浏览器会把它翻译成高效的机器码。Clang、CPython、R 和 SQLite 等真正的编译器和解释器本身就被编译成了 WebAssembly,只需下载一次,之后便在您自己的设备上编译或运行程序,不会向服务器发送任何内容。传统的在线编译器则恰恰相反:它们把您的代码上传到服务器,由服务器运行后再把输出返回给您。

要点速览
  • WebAssembly 是一种可移植的编译目标,所有主流浏览器都能以接近原生代码的速度运行它,并且与 JavaScript 处于同一套安全沙箱中。
  • 我们的 C 和 C++ 编译器在标签页中运行 Clang:它把您的代码转换成 WebAssembly 模块,再通过提供输入输出的小型 WASI 层来执行。
  • Python、R、SQL、Ruby、PHP、Lua 和 Prolog 以官方解释器的 WebAssembly 版本运行;JavaScript 系语言则使用浏览器自带的引擎。
  • 在本地运行意味着隐私保护、无需排队,而且运行时缓存之后还可以离线使用。
  • 代价是需要一次性下载、不支持线程和网络套接字、文件系统只是临时的内存文件系统,而且我们的 C++ 版本不支持异常。

WebAssembly 是什么

WebAssembly(常简称 Wasm)是一种低级指令格式,专门设计为编译目标。通常没有人手写它,而是由 Clang 或 Rust 编译器之类的编译器把普通源代码编译成 .wasm 文件,就像为 x86 或 ARM 处理器生成可执行文件一样。该格式是由 W3C 制定的开放标准,文档见 webassembly.org,Chrome、Edge、Firefox 和 Safari 均已支持。

以下三个特点让它很适合在浏览器中运行代码:

  • 可移植。同一个 .wasm 文件可以在 Windows、macOS、Linux、Android 和 iOS 上运行,不受处理器类型限制,因为浏览器会把它翻译成本机的机器码。
  • 速度快。WebAssembly 带有类型信息且贴近硬件,浏览器引擎可以在执行前把它编译成高效的机器码。对 CPU 要求高的代码通常比等效的 JavaScript 快得多,速度大致与原生代码处于同一量级,不过通常会略慢一些。
  • 沙箱隔离。WebAssembly 模块只有一块私有内存,只能调用明确提供给它的函数。除非宿主代码提供途径,否则它无法读取您的文件、建立网络连接或触碰页面的其他部分。

WASI:WebAssembly 的系统接口

C 程序需要操作系统:用来读取标准输入、写出输出、打开文件和获取时间。而 WebAssembly 本身并不内置这些功能。WebAssembly 系统接口(WASI)定义了一套标准的此类函数,供 WebAssembly 程序导入。在浏览器之外,Wasmtime 等运行时在真实操作系统之上实现 WASI;在浏览器内,则由名为 WASI shim 的小型 JavaScript 库,基于内存中的数据来实现这些函数。

传统在线编译器是如何工作的

大多数老牌在线编译器和编程练习网站都在自己的服务器上运行您的代码:

  1. 浏览器把源代码(以及任何输入)发送到网站的服务器。
  2. 服务器把它放入隔离环境中,通常是容器或轻量级虚拟机,并对时间、内存和进程数加以限制。
  3. 安装在那里的真正编译器或解释器负责构建并运行程序。
  4. 输出被捕获并发回您的浏览器,随后该环境被销毁。

这种模式确实有优点。服务器几乎可以提供任何语言和版本,以及线程、进程等完整的操作系统功能,有时还能访问网络和使用大型库。但是,您的代码会离开您的设备,繁忙的服务可能要排队,每次运行都依赖一次网络往返,而且运营方要为每次执行支付算力成本,这正是许多网站限制运行次数或要求注册账号的原因。

我们的浏览器端编译器如何工作

在我们的在线编译器上,每种语言的运行时都会下载到您的浏览器中,并在 Web Worker 里运行。Web Worker 是一个后台线程,可在程序运行期间保持页面响应。如果程序运行时间过长,例如陷入死循环,时间限制和“停止”按钮可以让页面终止该 worker。运行时文件都固定在确切的版本上,大多数文件在执行前还会用加密哈希进行校验。

C 和 C++:标签页里的真正编译器

C 语言在线编译器和 C++ 在线编译器使用基于 LLVM 的 Clang 21,由 YoWASP 项目打包为 WebAssembly。每次运行分为两个阶段:

  1. 编译。Clang 本身以 WebAssembly 形式运行,在开启优化和警告的情况下(C17 或 C++20)编译您的 main.c 或 main.cpp。它的输出不是 x86 或 ARM 可执行文件,而是一个面向 WASI 的新 WebAssembly 模块。编译器的警告和错误会与 Clang 打印的内容完全一致地显示出来。
  2. 运行。浏览器把该模块编译成机器码,并通过 WASI shim 启动它。shim 会把标准输入连接到输入框中的文本,并把标准输出和标准错误送到输出面板。

换句话说,编译成 WebAssembly 的编译器,又把您的程序编译成 WebAssembly,两者都在同一个标签页中运行。

编译成 WebAssembly 的解释器

语言运行时说明
PythonPyodide编译为 WebAssembly 的 CPython,即 Python 的参考解释器。NumPy 和 pandas 等软件包也已为其预先构建。
RwebRR 的官方 WebAssembly 版本,包含基础绘图功能。
SQLsql.js编译为 WebAssembly 的 SQLite 数据库引擎,数据库保存在内存中。
Rubyruby.wasm面向 WASI 构建的官方 CRuby 解释器,附带其标准库。
PHPphp-wasm编译为 WebAssembly 的 PHP 解释器。
Luawasmoon编译为 WebAssembly 的 Lua 5.4 参考解释器。
Prologswipl-wasm编译为 WebAssembly 的 SWI-Prolog。

由于这些就是人们安装在电脑上的同款解释器,它们的行为和错误信息与桌面版本高度一致。您可以在 Python 在线编译器、R 语言在线编译器和 SQL 在线编译器上试一试。

使用浏览器自带引擎的语言

有些语言完全不需要 WebAssembly。JavaScript 在线编译器直接在浏览器的 JavaScript 引擎中运行代码。TypeScript 和 CoffeeScript 由各自的官方编译器转换成 JavaScript(这些编译器本身就以 JavaScript 形式发布),转换结果再以同样方式运行。Scheme 使用 BiwaScheme,Clojure 使用 SCI(Small Clojure Interpreter,小型 Clojure 解释器),这两个解释器本身都以 JavaScript 运行。

在浏览器中运行代码的优势

  • 隐私。您的源代码和输入内容都留在您的设备上,不会被我们的服务器上传、存储或记录,因为服务器根本不会收到它们。
  • 无需排队,无需账号。每位访问者都使用自己的处理器,因此没有共享的执行队列,也不必限制运行次数。
  • 加载后可离线使用。页面和运行时下载并缓存之后,即使网络中断,只要标签页保持打开,您仍可继续编辑和运行代码。按需加载的额外软件包(例如某些 Python 库)在首次使用时仍需联网。
  • 重复运行很快。首次加载之后,解释型语言几乎可以瞬间启动,从点击“运行”到看到输出之间也没有网络往返。

局限与取舍

限制原因实际影响
下载体积整个编译器或解释器都必须传到您的浏览器。Clang、R 和 Ruby 有几十 MB;Lua 和 SQLite 不到 1 MB。之后再访问会使用浏览器缓存。
不支持线程我们的运行时是单线程的;WebAssembly 线程需要额外的浏览器隔离设置和运行时支持。pthread、std::thread 或 Python 的 threading 无法像在桌面上那样实现真正的并行。
不支持网络套接字沙箱不允许程序打开原始网络连接。在 C、Python 或 R 中打开套接字或发起 HTTP 请求的代码会失败。浏览器中的 JavaScript 仍可使用 fetch。
没有真正的文件系统运行时最多只能获得内存中的虚拟文件系统,绝不会触及您的磁盘。Python 等部分运行时可以在会话期间创建文件,但页面关闭后文件就会消失;我们的 C 和 C++ 程序只有标准输入和输出。
不支持 C++ 异常我们的 C++ 版本在编译时禁用了异常(-fno-exceptions)。使用 try、catch 或 throw 的代码无法编译;请改用返回值或错误码。
内存32 位 WebAssembly 最多只能寻址 4 GiB,而浏览器或设备允许的可能更少。非常大的数组或数据集可能出现内存不足的错误,在手机上尤其如此。
输入不是交互式的标准输入在程序启动之前就已提供。请先把全部输入填入输入框;提示符无法在运行过程中等您输入。

对于学习、测试算法和完成课程作业来说,这些限制很少会造成影响。如果程序需要联网、多进程或大量内存,请使用本地安装或基于服务器的环境。

安全模型

运行任意代码听起来有风险,但浏览器端编译器依靠的是浏览器本来就对每个网站施加的各种保护。

  • WebAssembly 隔离。模块只能读写自己的线性内存(有边界检查),也只能调用明确导入给它的函数。存在缓冲区溢出的 C 程序可能破坏自己的内存,但无法逃出到浏览器或操作系统中。
  • 浏览器沙箱。包括 WebAssembly 和 Web Worker 在内的所有页面代码,都运行在浏览器的进程沙箱和同源策略之内,因此无法读取其他网站的数据或您的文件。
  • Worker 与时间限制。在 worker 中运行意味着失控的程序可以被终止,而不会让页面卡死或需要重新加载。
  • 经过校验的运行时。固定确切的运行时版本,并在加载方式允许时校验哈希值,可以防止内容分发网络悄悄提供不同的或被篡改的文件。

需要注意一点:您在 JavaScript 在线编译器中运行的 JavaScript,与网页上的任何脚本具有相同的能力,包括发起网络请求。和运行任何代码一样,请只运行您看得懂的代码片段,尤其是从陌生人那里复制来的代码。

什么时候该用哪种在线编译器

  1. 如果您的代码属于隐私内容,例如课程作业或工作代码,请优先选择浏览器端编译器,这样代码不会离开您的设备。
  2. 如果您需要线程、套接字、特定的系统库,或者某种语言没有 WebAssembly 版本,请使用基于服务器的服务或本地安装。
  3. 如果网络较慢或按流量计费,请先从较轻量的运行时开始;大型运行时的首次加载是主要开销。

常见问题

使用你们的在线编译器时,我的代码会被发送到服务器吗?

不会。页面会下载编译器或解释器,您的代码和输入则在您自己的设备上编译和运行。服务器只负责提供页面和运行时文件。

WebAssembly 的速度和原生代码一样快吗?

通常很接近,但会稍慢一些,差距取决于具体任务和浏览器。对于一般的练习和算法,这点差别并不明显;首次下载才是更大的延迟。

为什么第一次运行会比较慢?

第一次运行需要下载并初始化运行时,对于 Clang、R 或 Ruby 来说就是几十 MB。浏览器会缓存这些文件,因此之后再访问会快得多。

为什么我用了 try 和 catch 的 C++ 代码无法编译?

我们的 C++ 版本在编译时禁用了异常,因为 WebAssembly 中的异常处理需要工具链和运行时提供额外支持,而这套方案并未使用。请把错误处理改写为使用返回值、std::optional 或错误码,或者在需要异常时改用本地编译。

在线编译器里的程序会损害我的电脑吗?

在浏览器中以 WebAssembly 形式运行的代码,被限制在自己的内存和页面提供的函数之内,浏览器沙箱又增加了一层保护。它无法读取您的文件或安装软件。不过 JavaScript 代码仍然可以像任何页面脚本一样发起网络请求,所以请避免运行您不信任的代码。

亲自试试

使用我们的免费工具,把本指南付诸实践。

继续阅读

已复制到剪贴板