Workers RPC 支持 Python/JavaScript Agent 直接交换对象引用并调用方法,无需定义 API/schema。大幅简化多语言协作。
两年前,我们推出了基于 Cap’n Proto RPC 构建的 Workers RPC。它让 Worker 能够调用其他 Worker 和 Durable Object 的方法、返回活对象并调用其方法,还能返回函数和流,并且无需定义 schema 或添加任何依赖,就能获得远程过程调用(RPC)系统的全部优势。我们称之为“JavaScript 原生 RPC”,因为它让 RPC 用起来就像这门语言原生支持的能力一样自然。
去年,我们将这套能力扩展到了 Web 浏览器与服务器之间,并推出了 Cap’n Web。
现在,我们要让它跨越不同语言。
通常,让使用不同语言编写的程序彼此通信是一件复杂的事:开发者往往需要构建自定义 API,或者采用 protobuf 这类与语言无关的序列化格式,才能让两个系统理解彼此。Workers 内置的 RPC 系统则能够在 JavaScript 与 Python 之间自动完成转换,无需任何额外工作。
现在,你可以从 JavaScript Worker 调用 Python Worker 中定义的方法,反过来也同样可以。你可以在 Python 与 JavaScript 之间共享对象,也可以从 TypeScript 调用 Python 对象的方法。一切都能直接工作。
如果你在一个使用 TypeScript 编写的 Worker 中定义了 add() 方法:
import { WorkerEntrypoint } from "cloudflare:workers";
export class RpcService extends WorkerEntrypoint {
async add(a: number, b: number): Promise<number> {
return a + b;
}
}
……那么你只需在 Python 中直接调用它:
from workers import Response, WorkerEntrypoint
class Default(WorkerEntrypoint):
async def fetch(self, request):
# Get the RPC stub from the TypeScript Worker.
rpc = self.env.RPC
# Call the TypeScript RPC method.
result = await rpc.add(42, 144)
return Response.json({"result": result})
不需要安装任何依赖。你只需要配置一个 Service binding:
"services": [
{
"binding": "RPC",
"service": "ts-rpc-server",
"entrypoint": "RpcService"
}
]
那么,你能用它做些什么?
这套 RPC 系统让你能够像使用一个库那样,构建复杂的多语言系统。下面是跨语言 RPC 提供的一些能力。
跨语言 RPC 调用的行为与普通函数调用一致:在 JavaScript/TypeScript 中返回 promise,在 Python 中返回 future。异常会沿调用链传播,并在 RPC 方法的调用位置抛出。
你可以将任何 Structured Cloneable 类型作为 RPC 调用的参数或返回值。这些值会被转换为 Python 中对应的类型,例如,JS Date 会转换为 Python datetime。
你可以将 JavaScript 函数传给 Python Worker,也可以将它们返回;反过来同样如此。当另一端调用传给它的函数时,系统会自动向你发起一次新的 RPC。
通常,对另一个 Worker 的 RPC 调用并不会经过网络。另一个 Worker 一般会与调用方运行在同一个线程中。与直接在同一个 Worker 中运行代码相比,它几乎没有额外的性能开销。
这套实现已经完全开源,属于 workerd 和 workers-runtime-sdk 的一部分。
要让 RPC 在 JavaScript Worker 与 Python Worker 之间实现无缝调用,主要障碍是如何打通两种语言各自不同的类型系统。JavaScript 开发者希望使用原生 JavaScript 类型,Python 开发者自然也希望使用原生 Python 类型。要连接两门拥有独立类型系统的语言,需要一套谨慎设计、目标明确的类型转换策略。
以两种语言处理函数参数的方式为例。在 JavaScript 中,定义复杂函数的一种典型方式,是将一个 Object 作为参数传入:
function myFunction(params: { key: string, value: boolean, optional?: number }) { ... }
// Called like this:
myFunction({ key: “myKey”, value: true, optional: 1 });
相比之下,Python 开发者通常会使用关键字参数来定义相同的函数:
def my_complex_function(key: str, value: bool, optional: int | None): ...
# Called like this:
my_complex_function(“myKey”, True, optional=1)
我们的目标,是让跨语言 RPC 完全透明。开发者应该感觉自己是在为单语言应用编写代码,不需要操心底层的转换机制。为此,我们将 Pyodide 的 Foreign Function Interface(FFI)与一套为 Python Workers 定制的类型转换层结合起来。
Pyodide 是编译为 WebAssembly 的 CPython 解释器,从 Python Workers 推出之初,它便一直为其提供支持。Pyodide 内置了一套可靠的 FFI,可以自动在 JavaScript 与 Python 类型之间进行转换。

当 Python Worker 通过 Service bindings 与 JavaScript Worker 通信时,Pyodide 的 FFI 会在 RPC 调用期间透明地转换对象。任何一端的开发者都不需要知道另一个 Worker 是用哪种语言编写的,一切都会在底层自动完成。
Pyodide 默认就能在两个环境之间映射原生类型:
JavaScript 对应类型
当无法进行直接转换时,例如遇到自定义 class 或函数,Pyodide 会创建一个 Proxy 对象。这个 proxy 会跨越语言边界转发属性访问和方法调用,因此可以支持将 Python 函数直接作为 callback 传给 JavaScript handler 之类的用法。
Pyodide FFI 还会将 Python 的关键字参数直接映射为 JavaScript 的对象式参数。例如,假设一个 JavaScript Worker 中有一个方法,它接收可选的 options 对象:
async get(key: string, options?: { type: string });
从 Python 调用这个 JavaScript Worker 时,你可以传入一个 Python dictionary 来表示 JavaScript 对象:
JSRPC.get("myKey", { "type": "text" })
不过,你也可以使用原生的 Python 关键字参数:
JSRPC.get("myKey", type="text")
Pyodide FFI 会将这两种调用都转换成 JavaScript Worker 所期望的准确结构,为 Python 开发者带来简洁、自然的 API 使用体验。
如需更深入地了解类型转换,可以参阅 Pyodide 文档。
虽然 Pyodide FFI 能够无缝转换标准内置类型,但它无法自动理解 Request、Response、Blob 或 File 等 Web API 对象。这些对象在 Cloudflare Workers 中十分常用,但 Python 并没有与它们直接对应的内置类型。
正如上一节所述,Pyodide 默认会将这些非标准对象视为 JavaScript Proxy。它不会将其转换成 Python 对象,而是创建一个透传 proxy,用来处理属性查询和方法调用。虽然这种方式可以正常工作,但它会将底层 JavaScript 实现的细节泄漏到 Python 中。Python 开发者必须时刻记住自己正在与 JavaScript proxy 交互,这会带来不必要的心智负担。
为了解决这个问题,我们推出了 workers-runtime-sdk Python package。它是一个专门用于处理 RPC 中自定义 Workers 类型的轻量转换层。当你使用 uv run pywrangler deploy 部署 Python Worker 时,这个 package 会被默认包含在内。事实上,只要你从 workers namespace 导入内容,就已经在使用它了:
from workers import Response
...
在底层,这个 SDK 会包装 bindings 提供的 RPC stub。它会拦截跨越语言边界的对象,并将其转换为 JavaScript Worker 和 Python Worker 都能自然使用的原生形式。
因此,Python 开发者可以继续使用熟悉且符合 Python 惯用写法的对象,让跨语言执行的存在感彻底消失。
你是否遇到过这种情况:想使用一个很棒的 Python package,但应用却是用 JavaScript 编写的?借助 Python Workers,你可以轻松做到这一点。来看一个例子。
Pygments 是一个流行的语法高亮 package,使用 Python 编写。要从 JavaScript 中使用它,你只需要在 Python Worker 中暴露一个调用 Pygments package 的方法。
在 JavaScript 中,我们可以通过访问 request 的 env 来调用这个方法:
export default {
async fetch(request, env) {
// Get the RPC stub from the Python Worker.
const rpc = env.PYTHON_RPC;
// Call the Python RPC method.
const result = await rpc.highlight_code('print(42)', 'python');
return Response.json(result);
}
}
接着在 Python 端,我们可以像下面这样定义一个包含该方法的 Python Worker:
from workers import WorkerEntrypoint
class Default(WorkerEntrypoint):
async def highlight_code(self, code: str, language: str) -> dict:
# Implementation goes here
现在,剩下的工作就是用 Python 编写执行高亮所需的代码。简化后的版本如下:
# ...
from pygments.formatters import HtmlFormatter
from pygments import highlight
from pygments.lexers import get_lexer_by_name
class Default(WorkerEntrypoint):
async def highlight_code(self, code: str, language: str) -> dict:
# Retrieve the lexer for the language specified.
lexer = get_lexer_by_name(language, stripall=True)
# Create the formatter and run the highlighter on the specified code.
formatter = HtmlFormatter(linenos=True, cssclass="highlight", style="monokai")
highlighted_html = highlight(code, lexer, formatter)
# Get the CSS for styling.
css = formatter.get_style_defs(".highlight")
return {
"html": highlighted_html,
"css": css
}
JavaScript 代码位于一个独立的 Worker 中,与 Python Worker 相互分离。因此,你还需要定义 Service bindings,确保它们可以互相通信。为此,可以将下面的配置放入 JavaScript Worker 的 wrangler.jsonc 文件:
"services": [
{
"binding": "PYTHON_RPC",
"service": "py-rpc-server"
}
]
这里的 service 名称需要与你的 Python Worker 名称一致。
要测试它们,可以打开两个独立的终端:在 JavaScript Worker 目录中运行 npx wrangler dev,同时在 Python Worker 目录中运行 uv run pywrangler dev。
GitHub 上提供了一个完整示例。你可以使用以下命令直接运行它:
git clone git@github.com:cloudflare/python-workers-examples.git
cd python-workers-examples/13-js-api-pygments/
# Terminal 1
cd ts/
npx wrangler dev
# Terminal 2
cd py/
uv run pywrangler dev
除了上面介绍的内容,我们的文档中还提供了更多 RPC 示例和相关信息。