6个版本 (3个重大更新)

0.4.0 2024年8月22日
0.3.0 2024年8月13日
0.2.1 2024年8月8日
0.2.0 2024年7月26日
0.1.1 2024年7月18日

#869 in 魔法豆

Download history 201/week @ 2024-07-18 172/week @ 2024-07-25 31/week @ 2024-08-01 285/week @ 2024-08-08 74/week @ 2024-08-15

每月下载量563次
用于 sails-idl-gen

GPL-3.0 许可证

64KB
1K SLoC

Sails  

Sails是一个库,它将使用Gear协议编写应用程序的体验提升到更简单、更清晰的层次。它处理诸如

  • 消除编写一些底层样板代码的必要性,让您专注于业务问题
  • 为您应用程序生成IDL文件
  • 生成的客户端允许您使用不同语言编写的代码与您的应用程序进行交互,并在不同的执行环境中执行

[!NOTE] Sails库以名称 sails-rscrates-io 上发布。

版本 "version <= 0.2.1" 钩定为 gear 库的 v1.4.2。

版本 "0.2.1 < version" 钩定为 gear 库的 v1.5.0。

入门

要么使用Sails CLI

cargo install sails-cli
cargo sails new-program my-ping

要么将以下内容添加到您的 Cargo.toml

[dependencies]
sails-rs = "*"
gstd = { version = "*", features = ["debug"] }

然后在您的 lib.rs

#![no_std]

use sails_rs::prelude::*;
use gstd::debug;

struct MyPing;

#[service]
impl MyPing {
    pub const fn new() -> Self {
        Self
    }

    pub async fn ping(&mut self) -> bool {
        debug!("Ping called");
        true
    }
}

#[derive(Default)]
struct MyProgram;

#[program]
impl MyProgram {
    #[route("ping")]
    pub fn ping_svc(&self) -> MyPing {
        MyPing::new()
    }
}

详细信息

Gear协议的整个想法是基于请求-响应模式的异步版本。加载到基于Gear网络的链上应用程序接收和处理来自其他链上或链下应用程序的消息。这两者都可以被视为您的应用程序提供的服务的外部消费者,后者可以代表与网络交互的普通人。

应用

Sails应用程序架构基于几个关键概念。

第一个是服务,它通过标记了#[service]属性的Rust结构体的实现来表示。服务的主要职责是实现应用程序业务逻辑的一些方面。

通过实现定义的一组服务公共方法本质上是一组服务对外部消费者公开的远程调用。每个在&mut self上工作的方法被视为改变某些状态的操作,而每个在&self上工作的方法被视为不改变任何东西并返回一些数据的查询。这两种类型的方法都可以接受客户端传递的参数,可以是同步的或异步的。其他所有服务的方法和关联函数都被视为实现细节并被忽略。由#[service]属性在服务背后生成的代码解码传入的请求消息,并根据方法名称将其调度到适当的方法。在方法完成时,其结果会被编码并作为响应返回给调用者。

#[service]
impl MyService {
    // This is a command
    pub fn do_something(&mut self, p1: u32, p2: String) -> &'static [u8] {
        ...
    }

    // This is a query
    pub fn some_value(&self, p1: Option<bool>) -> String {
        ...
    }
}

第二个关键概念是程序,与服务类似,它通过标记了#[program]属性的Rust结构体的实现来表示。程序的主要职责是托管一个或多个服务并将它们暴露给外部消费者。

它关联的一组返回Self公共函数被视为应用程序构造函数。这些函数可以接受客户端传递的参数,可以是同步的或异步的。其中之一将在应用程序生命周期的最开始被调用,即在应用程序被加载到网络上时。返回的程序实例将一直存在,直到应用程序保持在网络上。如果没有发现这样的方法,将生成一个具有以下签名的默认方法

pub fn default() -> Self {
    Self
}

程序的一组在&self上工作且没有其他参数的公共方法被视为公开服务构造函数,并在每次需要将传入的请求消息调度到所选服务时调用。其他所有方法和关联函数被视为实现细节并被忽略。由#[program]属性在程序背后生成的代码从网络接收传入的请求消息,解码它并将其调度到匹配的服务进行实际处理。之后,将结果编码并作为响应返回给调用者。每个应用程序只能有一个程序。

#[program]
impl MyProgram {
    // Application constructor
    pub fn new() -> Self {
        ...
    }

    // Yet another application constructor
    pub fn from_u32(p1: u32) -> Self {
        ...
    }

    // Service constructor
    pub fn ping_svc(&self) -> MyPing {
        ...
    }
}

最后一个关键概念是消息路由。这个概念在代码中没有强制表示,但可以通过应用上述公共方法和关联函数上的#[route]属性来修改。这个概念本身是关于使用服务和方法名称将传入的请求消息调度到特定服务的方法的规则。默认情况下,通过程序公开的每个服务都使用服务构造函数方法的名称转换为PascalCase来公开。例如

#[program]
impl MyProgram {
    // The `MyPing` service is exposed as `PingSvc`
    pub fn ping_svc(&self) -> MyPing {
        ...
    }
}

可以通过应用#[route]属性来改变这种行为

#[program]
impl MyProgram {
    // The `MyPing` service is exposed as `Ping`
    #[route("ping")] // The specified name will be converted into PascalCase
    pub fn ping_svc(&self) -> MyPing {
        ...
    }
}

相同的规则适用于服务方法名称

#[service]
impl MyPing {
    // The `do_ping` method is exposed as `Ping`
    #[route("ping")]
    pub fn do_ping(&mut self) {
        ...
    }

    // The `ping_count` method is exposed as `PingCount`
    pub fn ping_count(&self) -> u64 {
        ...
    }
}

事件

Sails提供了一种机制,在处理命令时从您的服务中发出事件。这些事件作为通知链外订阅者关于应用程序状态变化的手段。在Sails中,事件是通过events参数在#[service]属性中按服务配置和发出的。它们由Rust枚举定义,每个变体代表一个单独的事件及其可选数据。一旦服务声明它发出事件,#[service]属性就会自动生成notify_on服务方法。该方法可以被服务调用以发出事件。例如

fn counter_mut() -> &'static mut u32 {
    static mut COUNTER: u32 = 0;
    unsafe { &mut COUNTER }
}

struct MyCounter;

#[derive(Encode, TypeInfo)]
enum MyCounterEvent {
    Incremented(u32),
}

#[service(events = MyCounterEvent)]
impl MyCounter {
    pub fn new() -> Self {
        Self
    }

    pub fn increment(&mut self) {
        *counter_mut() += 1;
        self.notify_on(MyCounterEvent::Incremented(*counter_mut())).unwrap();
    }

    // This method is generated by the `#[service]` attribute
    fn notify_on(&mut self, event: MyCounterEvent) -> Result<()> {
        ...
    }
}

需要注意的是,内部上,事件使用与Gear协议中任何其他消息传输相同的机制。这意味着只有在发出事件命令成功完成后,事件才会被发布。

服务扩展(混入)

Sails的一个显著特点是它能够扩展(或混入)现有服务。这是通过使用extends参数在#[service]属性来实现的。假设您有服务A和服务B,可能来自外部包,并且您希望将它们的功能集成到新的服务C中。这种集成将导致服务AB的方法和事件无缝集成到服务C中,就像它们最初就是它的一部分一样。在这种情况下,服务C中可用的方法代表服务A和服务B的组合。如果出现方法名称冲突,即服务A和服务B都包含具有相同名称的方法,则extends参数中首先指定的服务的该方法具有优先权。这种策略不仅促进了功能的融合,还允许通过在新服务中定义具有相同名称的方法来覆盖原始服务中的特定方法。对于事件名称,不允许冲突。不幸的是,当报告为错误时,这是IDL生成过程的最早阶段。例如

struct MyServiceA;

#[service]
impl MyServiceA {
    pub fn do_a(&mut self) {
        ...
    }
}

struct MyServiceB;

#[service]
impl MyServiceB {
    pub fn do_b(&mut self) {
        ...
    }
}

struct MyServiceC;

#[service(extends = [MyServiceA, MyServiceB])]
impl MyServiceC {
    // New method
    pub fn do_c(&mut self) {
        ...
    }

    // Overridden method from MyServiceA
    pub fn do_a(&mut self) {
        ...
    }

    // do_b from MyServiceB will exposed due to the extends argument
}

有效负载编码

使用Sails编写的应用程序在基础层面使用SCALE Codec进行编码/解码数据。

每个传入的请求消息应具有以下格式

| SCALE编码的服务名称 | SCALE编码的方法名称 | SCALE编码的参数 |

每个传出的响应消息具有以下格式

| SCALE编码的服务名称 | SCALE编码的方法名称 | SCALE编码的结果 |

每个传出的事件消息具有以下格式

| SCALE编码的服务名称 | SCALE编码的事件名称 | SCALE编码的事件数据 |

客户端

与应用程序具有强大的交互能力至关重要。Sails提供了几种交互选项。

首先,它支持使用Gear协议进行手动交互。您可以使用

  • msg::send函数从gstd包中交互应用程序。
  • gclient 包用于从链下代码与链上应用交互。
  • @gear-js/api 库用于从 JavaScript 与你的程序交互。

你只需根据《负载编码》部分概述的布局组合一个字节负载,并将其发送到应用程序。

得益于生成的 IDL,Sails 以一种更清晰的方式提供了使用类似后者暴露的接口的生成客户端与你的应用交互的方式。目前,Sails 可以生成 Rust 和 TypeScript 的客户端代码。

关于 Rust,有两种选择

  • 使用可以为你编码和解码字节负载的生成代码,允许你继续使用发送原始字节的函数。
  • 使用完全生成的代码,可以以 RPC 风格与你的应用程序交互。

有关 TypeScript 的信息,请参阅 生成客户端 文档。

假设你有一个应用程序公开了名为 MyService 的服务,该服务有一个名为 do_something 的命令。

struct Output {
    m1: u32,
    m2: String,
}

#[service]
impl MyService {
    pub fn do_something(&mut self, p1: u32, p2: String) -> Output {
        ...
    }
}

#[program]
impl MyProgram {
    pub fn my_service(&self) -> MyService {
        MyService::new()
    }
}

然后在提供的客户端应用程序中,代码生成在 Rust 构建脚本中完成,你可以像这样使用生成的代码(选项 1)

include!(concat!(env!("OUT_DIR"), "/my_service.rs"));

fn some_client_code() {
    let call_payload = my_service::io::DoSomething::encode_call(42, "Hello".to_string());
    let reply_bytes = gstd::msg::send_bytes_for_reply(target_app_id, call_payload, 0, 0).await.unwrap();
    let reply = my_service::io::DoSomething::decode_reply(&reply_bytes).unwrap();
    let m1 = reply.m1;
    let m2 = reply.m2;
}

或者像这样(选项 2)

include!(concat!(env!("OUT_DIR"), "/my_service.rs"));

fn some_client_code() {
    let mut my_service = MyService::new(remoting); // remoting is an abstraction provided by Sails
    let reply_ticket = client.do_something(42, "Hello".to_string())
        .with_reply_deposit(42)
        .publish(target_app_id)
        .await.unwrap();
    let reply = reply_ticket.reply().await.unwrap();
    let m1 = reply.m1;
    let m2 = reply.m2;
}

第二种选项为你提供了代码可测试的选项,因为生成的代码依赖于可以轻松模拟的特质。

关于 TypeScript,可以使用 sails-js 库与程序交互。有关更多详细信息,请参阅 sails-js 文档

示例

你可以在这里找到所有示例,以及在每个文件夹级别提供的某些描述。你还可以在代码中找到一些解释性注释。以下是上述功能及其在示例中展示的简要概述

通过程序公开服务

示例遵循几个程序公开几个服务的原则。请参阅 DemoProgram,该程序展示了这一点,包括程序多个构造函数的使用以及暴露服务的 #[route] 属性。该示例还包括 Rust 构建脚本,该脚本将程序构建为准备在 Gear 网络上加载的 WASM 应用。

基本服务

有几个服务演示了基本的服务结构,公开了一些基于输入参数操作并返回一些结果的原生方法。它们是开发你服务的绝佳起点。请参阅 PingThisThat 服务。后者除了基本内容外,还展示了可以作为服务方法参数和返回值使用的各种类型。

处理数据

在现实世界中,几乎所有的应用程序都会处理某种形式的数据,使用 Sails 开发的应用程序也不例外。如《应用程序》部分所述,为每个传入请求消息实例化服务,表明这些服务是无状态的。然而,有几种方法可以使你的服务保持一些状态。在这种情况下,状态将被视为服务外部。

最推荐的方法在Counter服务中展示,数据作为程序的一部分存储,并通过RefCell传递给服务。服务模块仅定义数据的形状,但需要从外部传递数据本身。此选项为您提供完全的灵活性,并允许您在多线程环境中对服务进行单元测试,确保测试不会相互影响。

另一种方法在RmrkCatalogRmrkResource服务中展示,数据存储在服务模块的静态变量中。这种策略确保状态完全对外隐藏,使服务完全自包含。然而,这种方法在多线程环境中的单元测试并不理想,因为每个测试都可能影响其他测试。此外,在首次使用服务之前,不要忘记调用服务的seed方法。

您还可以探索其他方法,例如使服务需要&'a mut数据(这使得服务不可克隆),或者使用Cell(这需要数据复制,产生额外成本)。

在所有场景中,除了使用Cell外,在服务方法中的异步调用期间考虑数据的静态性质至关重要。这意味着在启动异步调用之前访问的数据在调用完成时可能会发生变化。有关详细信息,请参阅RmrkResource服务的add_part_to_resource方法。

事件

您可以在CounterRmrkResource服务中找到如何从您的服务中发出事件的示例。

服务扩展(混入)

服务扩展的示例通过Dog服务演示,该服务扩展了来自同一crate的Mammal服务以及来自不同crate的Walker服务。被扩展的服务必须实现Clone特质,而扩展服务必须实现扩展服务的AsRef特质。

使用Rust生成的客户端

Demo Client crate展示了如何将IDL文件生成客户端代码作为单独的Rust crate。或者,您可以直接在应用程序crate中使用相同的方法。有关详细信息,请参阅Rmrk Resource

您可以在Demo Tests中找到如何使用生成的客户端代码与应用程序交互的多种示例。请在代码注释中查看更多详细信息。

由于生成的代码对所有环境都相同,无论是来自测试还是来自另一个应用程序的交互,这些交互的技术也是相同的。您可以在Rmrk Resource服务的add_part_to_resource方法中找到来自应用程序的交互示例。

请注意,使用生成的客户端需要将sails_rs crate添加到依赖项中。

许可证

本软件受Apache License, Version 2.0MIT许可证的许可,具体选择由您决定。 除非您明确声明,否则根据Apache-2.0许可证定义的,您有意提交到Sails的贡献,将以上述方式双重许可,不附加任何额外条款或条件。

依赖项

~6–29MB
~460K SLoC