Skip to main content

ckb-script-ipc IPC

In Inter-Process Communication (IPC) in Scripts, we introduced the basic concepts of IPC. This article further explains how to use ckb-script-ipc in Rust for inter-process communication.

ckb_script_ipc::service

The compiler can provide some basic implementations of ckb spawn for a trait through the service attribute.

This trait should be placed in a shared library accessible to both client and server scripts. The #[ckb_script_ipc::service] attribute macro automatically generates the necessary implementations for IPC communication.

Syntax

pub fn service(_attr: TokenStream, input: TokenStream) -> TokenStream

Example

A Simplified IPC Solution

To address the complexities of implementing IPC from scratch, we’ve developed ckb-script-ipc, a library that significantly simplifies the process. This library draws inspiration from Google’s tarpc and provides a straightforward, easy-to-use interface for IPC implementation.

Let’s walk through the implementation process step by step.

  1. First, add the required dependencies to your Cargo.toml:
ckb-script-ipc = { version = "..." }
ckb-script-ipc-common = { version = "..." }
serde = { version = "...", default-features = false, features = ["derive"] }

Remember to replace “…” with the latest available versions of these crates.

  1. Define your IPC interface using a trait decorated with our service attribute:
#[ckb_script_ipc::service]
pub trait World {
fn hello(name: String) -> Result<String, u64>;
}

This trait should be placed in a shared library accessible to both client and server scripts. The #[ckb_script_ipc::service] attribute macro automatically generates the necessary implementations for IPC communication.

note

The terms Client and Server are used here by analogy. If we think in terms of an HTTP server, the Server is like Nginx—responsible for listening and responding—while the Client acts as the requester.

In the context of IPC, the Server provides certain capabilities (i.e., interfaces), and the Client invokes them.

In most cases, the parent process acts as the Client, while the child process plays the role of the Server.

  1. Initialize the server by creating communication pipes:
use ckb_script_ipc_common::spawn::spawn_server;

let (read_pipe, write_pipe) = spawn_server(
0,
Source::CellDep,
&[CString::new("demo").unwrap().as_ref()],
)?;
  1. Implement the service logic and start the server:
use crate::def::World;
use ckb_script_ipc_common::spawn::run_server;

struct WorldServer;

impl World for WorldServer {
fn hello(&mut self, name: String) -> Result<String, u64> {
if name == "error" {
Err(1)
} else {
Ok(format!("hello, {}", name))
}
}
}

run_server(WorldServer.server()).map_err(|_| Error::ServerError)

Note that run_server operates as an infinite loop to handle incoming requests. The server() method is automatically implemented by our proc-macro.

  1. Set up and interact with the client:
use crate::def::WorldClient;
let mut client = WorldClient::new(read_pipe, write_pipe);
let ret = client.hello("world".into()).unwrap();

The client uses the pipe handles obtained during server initialization to communicate with the server process. For a complete working example, you can explore our rust-script-examples/ipc-parent repository.

This implementation abstracts away the complexities of IPC communication, handling serialization, message passing, and error management internally. Developers can focus on defining their service interfaces and implementing business logic rather than dealing with low-level IPC details.

Details of ckb-script-ipc Implementation

Procedural Macros

The implementation of client-server communication in ckb-script-ipc heavily relies on Rust’s procedural macros to eliminate boilerplate code. The #[ckb_script_ipc::service] attribute macro is particularly powerful, automatically generating the necessary code for client, server, and communication handling.

Let’s examine how this macro transforms a simple service definition into production-ready code:

First, we define our service interface:

#[ckb_script_ipc::service]
pub trait World {
fn hello(name: String) -> Result<String, u64>;
}

The macro then generates the required implementation code, including client-side methods, request/response types, and communication handling. Here’s a simplified version of the generated client code:

impl<R, W> WorldClient<R, W>
where
R: ckb_script_ipc_common::io::Read,
W: ckb_script_ipc_common::io::Write,
{
pub fn hello(&mut self, name: String) -> Result<String, u64> {
let request = WorldRequest::Hello { name };
let resp: Result<_, ckb_script_ipc_common::error::IpcError> = self
.channel
.call::<_, WorldResponse>("World.hello", request);
match resp {
Ok(WorldResponse::Hello(ret)) => ret,
Err(e) => {
// Error handling code
}
}
}
}

Here is a simplified version of generated server code:

impl<S> ckb_script_ipc_common::ipc::Serve for ServeWorld<S>
where
S: World,
{
type Req = WorldRequest;
type Resp = WorldResponse;
fn serve(
&mut self,
req: WorldRequest,
) -> ::core::result::Result<
WorldResponse,
ckb_script_ipc_common::error::IpcError,
> {
match req {
WorldRequest::Hello { name } => {
let ret = self.service.hello(name);
Ok(WorldResponse::Hello(ret))
}
}
}
}

The generated code handles several aspects:

  • Type-safe request and response structures
  • Proper error handling and propagation
  • Serialization and deserialization of parameters
  • Method routing and dispatch

This automatic code generation significantly reduces development time and potential errors while ensuring consistent implementation patterns across different services.

ckb_script_ipc::service

As mentioned earlier, by simply adding the #[ckb_script_ipc::service] attribute macro to a trait, you can automatically generate all necessary IPC-related code. This section uses the rust-script-examples repository to illustrate how this macro works and what code it generates.

The IPC example is divided into three parts:

  • crate/ipc-interface: Defines the IPC interface World.
  • contracts/ipc-parent: Acts as the IPC client. It uses spawn to start the ipc-child process and communicates with it.
  • contracts/ipc-child: Implements the IPC server and handles interface calls.

Once #[ckb_script_ipc::service] is applied, the following key functions are automatically generated:


WorldClient::new

Syntax

pub fn new(reader: R, writer: W) -> Self

Parameters

  • reader: A readable file descriptor.
  • writer: A writable file descriptor.

Return

A WorldClient instance that can be used to send requests to the server.


WorldClient::hello

This function is auto-generated based on the method defined in the user-provided trait. The hello method used to call the Server.

Syntax

pub fn hello(&mut self, name: String) -> Result<String, u64>

Remarks

To inspect the macro-expanded code, simply run cargo expand in the examples/rust-script-examples/crate/ipc-interface directory. For example:

pub struct WorldClient<R, W>
where
R: ckb_script_ipc_common::io::Read,
W: ckb_script_ipc_common::io::Write,
{
channel: ckb_script_ipc_common::channel::Channel<R, W>,
}

WorldClient is an auto-generated struct that holds a channel object internally to handle IPC communication.

Here is the implementation of the hello method:

pub fn hello(&mut self, name: String) -> Result<String, u64> {
let request = WorldRequest::Hello { name };
let resp: Result<_, ckb_script_ipc_common::error::IpcError> = self
.channel
.call::<_, WorldResponse>("World.hello", request);
match resp {
Ok(WorldResponse::Hello(ret)) => ret,
Err(e) => panic!("IPC error: {:?}", e),
_ => panic!("IPC error: wrong method id"),
}
}

This method packages the parameters into a request enum and calls the underlying channel’s call method to send the message. The request enum is defined as:

pub enum WorldRequest {
Hello { name: String },
}

This enum is generated by the macro and implements _serde::Serialize and _serde::Deserialize to handle serialization and deserialization.

Although the underlying IPC layer does not mandate a specific data format, we use JSON for better cross-language compatibility.

After the request is sent, the child process responds with a WorldResponse, which works similarly to WorldRequest and is not repeated here.