#rpc #msgpack #ipc #lavish

app lavish-compiler

Lavish IDL 编译器

7 个不稳定版本 (3 个重大更改)

0.4.0 2019年6月25日
0.3.3 2019年6月14日
0.2.0 2019年6月13日
0.1.0 2019年6月6日

#28 in #msgpack

Download history 19/week @ 2024-03-29 8/week @ 2024-04-05 2/week @ 2024-06-28 51/week @ 2024-07-05

每月下载量 53次

MIT 协议

145KB
4K SLoC

lavish-compiler

Build Status MIT licensed

lavish 允许您声明服务,并轻松地从多种语言中实现/消费它们。

具有意见

  • 拥有自己的模式语言和编译器(用Rust编写)
  • 目前针对Rust、Go和TypeScript
  • 自带Rust的RPC运行时
  • 设计时考虑到“双向帧MessagePack-RPC over TCP”

状态

lavish仍在开发中,目前不可用

  • 预发布:规则语言,模式语言
  • 预发布: 解析器检查器
  • Rust
  • Go
    • 仅研究:运行时
    • 尚未开始:代码生成
  • TypeScript
    • 仅研究:运行时
    • 尚未开始:代码生成

模式

模式可以定义“函数”,它接受参数并返回结果。

server fn log(message: string)
server fn get_version() -> (major: i64, minor: i64, patch: i64)
server fn shutdown()

所有输入参数和输出参数(结果)都有名称。

函数可以有命名空间

namespace utils {
    server fn log()
    server fn get_version()
}

namespace system {
    server fn shutdown()
}

函数可以由服务器或客户端实现

namespace session {
    // try to log in. if password authentication is
    // enabled, @get_password is called.
    server fn login(username: string)

    client fn get_password(username: string) -> (password: string)

    server fn logout()
}

内置类型(小写)包括:

  • i8u16u32u64:无符号整数
  • i8i16i32i64:有符号整数
    • 注意JavaScript只有53位精度。
  • f32f64:浮点数
  • bool:布尔值
  • string:UTF-8字符串
  • data:原始字节数组
  • timestamp:UTC日期+时间

可以声明自定义类型,那些应该是 CamelCase

enum LoginType {
    Anonymous = "anonymous",
    Password = "password",
} 

struct Session {
    login_type: LoginType,
    connected_at: timestamp,
}

namespace session {
    server fn login() -> (session: Session)
    // etc.
}

默认情况下,所有字段都必须指定 - 没有默认值。然而,可以使用 option<T> 将字段设置为可选。

// password can be None in Rust, nil in Go, undefined in TypeScript
server fn login(password: option<string>)

数组使用 array<T> 声明。

server fn login(ciphers: array<Cipher>)

映射使用 map<K, V> 声明。

server fn login(options: map<string, string>)

optionmaparray 可以嵌套。

server fn login(options: option<map<string, string>>)

可以导入第三方模式。

import itchio from "github.com/itchio/go-itchio"

namespace fetch {
    server fn game(id: i64) -> (game: option<itchio.Game>)
}

(关于导入机制的更多信息将在后面介绍。)

工作区

工作区是一个包含 lavish-rules 文件的目录。

lavish-rules 文件类似于 lavish 的 Makefile - 它告诉 lavish 编译什么,以及编译哪种语言。

lavish 命令行工具将模式文件编译成 Rust、Go、TypeScript 代码。

每个工作区

  • 针对单一语言(Rust、Go、TypeScript)
  • 可以构建各种服务
    • ...它们共享导入

创建时钟服务

假设我们正在编写一个简单的 Go 服务,该服务返回当前时间。

在运行 lavish 编译器之前,我们的存储库看起来像

- go.mod
- main.go
- services/
  - clock.lavish
  - lavish-rules

clock.lavish 包含

server fn current_time() -> (time: timestamp)

并且 lavish-rules 包含

target go

build clock from "./clock.lavish"

lavish 编译器接受工作区的路径

lavish build ./services

运行 lavish 编译器后,我们的存储库将看起来像

- go.mod
- main.go
- services/
  - clock.lavish
  - lavish-rules
  - clock/       <-- generated
    - clock.go   <-- generated

现在我们可以实现时钟服务器,例如

package main

import (
    "github.com/fasterthanlime/clock/services/clock"
    "time"
)

func Handler() clock.ServerHandler {
    var h clock.ServerHandler

    h.OnCurrentTime(func () (clock.CurrentTimeResults, error) {
        res := clock.CurrentTimeResults{
            time: time.Now(),
        }
        return res, nil
    })

    return h
}

最后,我们可以在顶级添加一个 lavish-rules 文件,以便我们可以在以后的项目中无缝导入它

export "./services/clock.lavish" as clock

从 Rust 消费时钟服务

假设我们想从 rust 调用我们的时钟服务。

我们的初始 Rust 存储库将看起来像

- Cargo.toml
- src
  - main.rs
  - services/
    - lavish-rules

我们的 lavish-rules 文件将看起来像

target rust

build clock from "github.com/fasterthanlime/clock"

使用以下命令运行编译器

lavish build ./src/services

...将会抱怨 clock 缺失。

运行

lavish fetch ./src/services

将会填充 lavish-vendor 文件夹

- Cargo.toml
- src
  - main.rs
  - lavish-rules
  - lavish-vendor/  <-- new
    - clock.lavish  <-- new

再次运行编译将生成 rust 代码

- Cargo.toml
- src
  - main.rs
  - lavish-rules
    - lavish-vendor/
    - clock.lavish
  - clock/          <-- new
    - mod.rs        <-- new

现在,可以从 Rust 导入 clock 模块并用于消费服务,例如

mod clock;

type Error = Box<dyn std::error::Error + 'static>;

async fn example() -> Result<(), Error> {
    // Create router - don't implement any functions from our side.
    let r = clock::client::Router::new();

    // Connect to server over TCP, with default timeout.
    let client = lavish::connect(r, "localhost:5959")?.client();

    {
        let time = client.call(clock::current_time::Params {})?.time;
        println!("Server time: {:#?}", time);
    }

    // when all handles go out of scope, the connection is closed
}

从 TypeScript 消费时钟服务

初始存储库

- src/
  - main.ts
  - services/
    - lavish-rules

lavish-rules 的内容

target ts

build clock from "github.com/itchio"

lavish fetch src/services

- src/
  - main.ts
  - services/
    - lavish-rules
    - lavish-vendor/  <-- new
      - clock.lavish  <-- new

lavish compile src/services

- src/
  - main.ts
  - services/
    - lavish-rules
    - lavish-vendor/
      - clock.lavish
    - clock        <-- new
      - index.ts   <-- new

然后我们可以从 index.ts 使用它

import clock from "./services/clock"

async function main() {
    let socket = new net.Socket();
    await new Promise((resolve, reject) => {
        socket.on("error", reject);
        socket.connect({ host: "localhost", port: 5959 }, resolve);
    });

    let client = new clock.Client(socket);
    console.log(`Server time: `, await client.getTime());
    socket.close();
}

main().catch((e) => {
    console.error(e);
    process.exit(1);
});

那很好,但...(常见问题解答)

为什么使用工作区?

假设你使用两个服务,AB,并且它们都使用来自模式 C 的类型。

你希望能够将来自 A 的调用结果作为参数传递给 B 的调用。

如果你在同一工作区中构建 AB,你最终将得到三个目录:ABCAB 都将使用来自 C 的类型。

此外

  • 传递数百万个命令行选项没有乐趣
  • 数百万个环境变量也没有乐趣
  • 一个最小的配置语言(lavish-rules)实际上并不那么糟糕
  • 无论写一个还是两个解析器,差别都不大

如果A和B导入不同的C会发生什么?

那么你无法在同一个工作区中使用AB。不过你可以创建两个工作区!

这似乎是一个任意的限制。这会简化实施过程吗?

会的,非常简化。

为什么每个工作区只能有一个目标?

再次,简化实施。如果你想在单个仓库中为多种语言生成绑定,你可以有

- foobar/
  - lavish-rules
  - foobar-js/
    - lavish-rules
  - foobar-go/
    - lavish-rules
  - foobar-rs/
    - lavish-rules

import from路径的格式是什么?

我对导入语法的想法是,对于本地文件

import foo from "./foo.lavish"
import bar from "../bar.lavish"

对于仓库

import foo from "github.com/user/foo"
import foo from "gitlab.com/user/bar"

它是如何知道要git clone什么内容的?

给定host/user/project,它尝试

  • https://host/user/project.git
  • git@host:user/project.git

所以lavish build需要网络连接吗?

不,不需要。lavish fetch需要。

所以lavish fetch可以算是一个迷你包管理器吗?

是的,可以。我中招了。另一种选择似乎需要复制大量文件或者手动克隆仓库,这在很多方面都很糟糕。

TL;DR: lavish fetch负责获取依赖,lavish build可以离线工作。

它与其他项目相比如何?

我非常喜欢JSON-RPC,因为它的简单性。那是我之前用的。Msgpack-RPC非常相似,只是序列化更快,有合适的时间戳类型,并且可以传递原始字节。

Cap'n Proto RPC非常令人敬畏。它不仅速度快,还带来了独特的功能——能力和承诺流水线。我对此感到非常兴奋。

然而,在花了些时间在现有的TypeScript序列化库之上实现capnp-rpc后,我最终承认

  • 实现复杂度对我来说太高了。从头开始写另一个实现(针对新语言)需要大量的工作,我不懂Rust实现,如果出了问题,我会很难追踪。
  • 能力使其难以在浏览器中使用。这不是巧合,对于JavaScript世界,推荐的实现是仅限node(绑定到C++库)。尽管我设法在纯TypeScript中实现了RPC,但我不得不使用electron和node特定的功能来 hook into GC(以便知道何时释放能力)。浏览器使用很容易泄漏能力,而浏览器出于安全原因并不想暴露GC hooks。
  • 它是专门设计的。没有强烈的愿望去推广其采用。它正在内部使用,但开发者没有兴趣让它成为适合所有人的万能工具——这很正常!这正是我在lavish中所做的。

tarpc看起来很棒,但仅限Rust。

grpc显然试图成为适合所有人的万能工具。我希望从用各种语言编写的各种应用程序中消费服务——一个MsgPack序列化库加上TCP套接字是合理的要求。ProtoBufs + HTTP/2不是。

依赖项

~5–15MB
~151K SLoC