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 日 |
2 in #gear
每月 565 次下载
用于 4 个crate(通过 sails-macros)
64KB
1.5K SLoC
Sails
Sails 是一个库,用于将使用 Gear 协议 编写应用程序的经验提升到更简单、更清晰的层次。它处理如下事项:
- 消除编写某些低级样板代码的必要性,让您专注于业务问题
- 为您的应用程序生成 IDL 文件
- 生成客户端,允许您从编写不同语言并执行在不同运行时运行的代码中与您的应用程序交互
[!注意] Sails 库以
sails-rs
的名称在crates-io
上发布。版本 "<= 0.2.0" 锁定到 gear 库的 v1.4.2 版。
入门指南
将以下内容添加到您的 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 结构体 impl 表示。服务的主要责任是实现应用程序业务逻辑的一些方面。
由impl定义的一组服务的公共方法本质上是服务对外部消费者暴露的远程调用集。每个在&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的一个显著特点是它能够扩展(或混合)现有服务。这是通过在#[service]
属性中使用extends
参数来实现的。假设您有服务A
和服务B
,它们可能来自外部包,您希望将它们的功能集成到新的服务C
中。这种集成将导致服务A
和服务B
的方法和事件无缝集成到服务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 的 构建脚本,它将程序作为 WASM 应用程序构建,准备加载到 Gear 网络上。
基本服务
有几个服务展示了基本的服务结构,公开了一些基于输入参数操作并返回某些结果的原始方法。它们是开发您自己的服务的极好起点。请参阅 Ping 和 ThisThat 服务。后者除了基本功能外,还展示了可以在服务方法中使用作为参数和返回值的多种类型。
与数据协同工作
在现实世界中,几乎所有应用程序都使用某种形式的数据,使用 Sails 开发的应用程序也不例外。正如在《应用程序》部分讨论的那样,为每个传入的消息请求实例化服务,这表明这些服务是无状态的。然而,有几种方法可以启用您的服务以维护一些状态。在这种情况下,该状态将被视为服务之外的外部状态。
最推荐的方法是在Counter服务中展示,其中数据作为程序的一部分存储,并通过RefCell
传递到服务中。服务模块仅定义数据的形状,但需要从外部传递数据本身。此选项为您提供了完全的灵活性,并允许您在多线程环境中对服务进行单元测试,确保测试不会相互影响。
另一种方法在RmrkCatalog和RmrkResource服务中展示,其中数据存储在服务模块中的静态变量中。这种策略确保状态完全对外隐藏,使服务完全自包含。然而,这种方法在多线程环境中的单元测试并不理想,因为每个测试都可能影响其他测试。此外,在第一次使用服务之前,不要忘记调用服务的seed
方法。
您还可以探索其他方法,例如使服务的数据需要&'a mut
(这使得服务不可克隆),或使用Cell
(这需要数据复制,产生额外的成本)。
在所有情况下,除了使用Cell
外,在服务方法中的异步调用期间,考虑数据的静态性质至关重要。这意味着在开始异步调用之前访问的数据可能在调用完成时发生变化。有关更多详细信息,请参阅RmrkResource服务的add_part_to_resource
方法。
事件
您可以在Counter和RmrkResource服务中找到如何从服务中发出事件的示例。
服务扩展(混合)
使用Rust生成的客户端的示例以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
库包含在依赖项中。
许可证
根据您的选择,许可协议可以是Apache许可证,版本2.0或MIT许可证。除非您明确表示,否则您提交给Sails以包含在内的任何有意贡献,根据Apache-2.0许可证的定义,应按上述方式双许可,不附加任何额外条款或条件。 除非您明确表示,否则任何有意提交给Sails并由您作为Apache-2.0许可证中定义的贡献,应按上述方式双许可,不附加任何额外条款或条件。
lib.rs
:
通过sails-macros
库公开的进程宏的实现。
依赖项
~1.1–1.6MB
~32K SLoC