Calling Other Services

We covered how to create transactions in test cases which call services in the previous chapter. Now we'll cover how services may call each other.

A Tale of Two Services

Let's create two services, arithmetic and arithmetic2.

cargo new --lib arithmetic
cargo new --lib arithmetic2

# arithmetic needs the psibase library
cd arithmetic
cargo add psibase

# arithmetic2 needs both psibase and arithmetic
cd ../arithmetic2
cargo add psibase
cargo add --path ../arithmetic

This creates the following tree:

.
├── arithmetic
│   ├── Cargo.lock
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
└── arithmetic2
    ├── Cargo.lock
    ├── Cargo.toml
    └── src
        └── lib.rs

Replace arithmetic/src/lib.rs with the following:

#[psibase::service]
mod service {
    #[action]
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
}

Replace arithmetic2/src/lib.rs with the following:

#[psibase::service]
mod service {
    #[action]
    fn mult_add(a: i32, b: i32, c: i32, d: i32) -> i32 {
        // Synchronous call to other service
        arithmetic2::Wrapper::call().add(a * b, c * d)
    }
}

// Our test case needs both services to function
#[psibase::test_case(services("arithmetic", "arithmetic2"))]
fn test_arith(chain: psibase::Chain) -> Result<(), psibase::Error> {
    // Verify arithmetic1 works
    assert_eq!(arithmetic1::Wrapper::push(&chain).add(3, 4).get()?, 7);

    // Verify arithmetic2 works
    assert_eq!(Wrapper::push(&chain).mult_add(3, 4, 5, 6).get()?, 42);
}

Try Them Out

Run the following from arithmetic2/. This will build both services, build the test, and run it.

cargo psibase test

Recursion

Library Or Program?

We use cargo new --lib to create services. The top level is lib.rs instead of main.rs. Does this mean that psibase services are libraries?

They are in some ways, but not in others. The WASM spec doesn't distinguish between libraries and programs. A WASM file contains a set of exported functions which things on the outside can call into. It also declares a set of imported functions that allow it to call things outside of itself. This makes WASMs a lot like libraries, but there's also a concept that makes them a lot like programs: memory isolation. Unless a WASM exports its memory so that other WASMs can import it, or imports its memory so another WASM can provide it, the WASM's address space isn't accessible to other WASMs, just like a program. Psibase doesn't currently allow services to share memory with each other; it copies data between them when necessary.

Cargo and the Rust compiler can build WASMs from either libraries or programs. They have this tradeoff:

  • A package may provide any number of programs, but at most 1 library.
  • Programs and libraries may use definitions from other libraries, but not from other programs.

To make it easy for Rust service A to call Rust service B, A needs to use wrapper definitions from B. If B is a library, e.g. lib.rs, then this is easy. If B is a program, e.g. main.rs, then this is impossible unless B's package defines both a program and a library. This would have complicated the psibase::service attribute macro and forced service authors to split their code between header-like files and implementation-like files. We avoided this in most cases by deciding that Rust services will be libraries. Both the service macro and cargo-psibase make this assumption. We made the opposite choice for C++ services; it's easier and cleaner for CMake to build WASM from programs than it is from libraries.