Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom errors #977

Merged
merged 11 commits into from
Jan 24, 2023
14 changes: 8 additions & 6 deletions core/src/server/rpc_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,14 +569,15 @@ impl<Context: Send + Sync + 'static> RpcModule<Context> {
}

/// Register a new asynchronous RPC method, which computes the response with the given callback.
pub fn register_async_method<R, Fun, Fut>(
pub fn register_async_method<R, E, Fun, Fut>(
Copy link
Collaborator

@jsdw jsdw Jan 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering whether we'd gain a bit more flexibility ultimately by copying Axum a bit and having a trait which is something like

trait IntoResponse {
    fn into_response(self) -> MethodResponse;
}

This could then be impl'd for Result<V, E> where V: Serialize, E: Into<Error> for instance:

impl <V, E> IntoResponse for Result<V, E> 
where
    V: Serialize,
    E: Into<Error>
{
    fn into_response(self) -> MethodResponse {
        // look at impl MethodResponse::response/error and use this here
    }
} 

fns like this register_method would expect a callback which was like Fn(Params, &Context) -> R (where R: IntoResponse) and would call into_response() on the result (perhaps passing in any specific bits they need to as args to that).

Then we could impl things like From<Infallible> for Error to allow Result<V, Infallible> responses for instance, and perhaps have more room then to consider supporting other response types than Result in the future.

Just thinking out loud really; maybe for now it's fine just to support custom errors but still expect a Result back..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, I considered it, but ultimately decided against doing huge changes in a single PR. This is something that, I think, can be added later iteratively - and, also, maybe someone else would like to tackle that instead of me. :D I'm fine with looking into it, but I'm also happy to share the fun.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose that we split the error layers into jsonrpsee's and application level, and, further, split the jsonrpsee's error from one single struct into separate ClientError and ServerError as there are very distinctly different in handling. I think it would significantly improve the ergonomics, and unlock more explicit and sensible error customization for the client side, as this PR only addresses the server side really.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you like the direction I proposed I'll create an issue for splitting the errors and mark this discussion as resolved.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@niklasad1 whatcha reckon? I think you were less keen on splitting the errors?

But in any case, I think for now I'm fine with just allowing the custom error and not being more general; perhaps in some future iteration we can add the above sort of thing if it makes sense :)

Copy link
Member

@niklasad1 niklasad1 Jan 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be weird to keep the proc macro API i.e, to share the same definition with different error types for the client and server. It's still possible to do it in the proc macro code as you have changed it this PR.

Example both client and server

struct CustomError;

#[rpc(server, client)]
pub trait Rpc
{
	// The client will return `ClientError` here.....
	async fn f(&self) -> Result<String, CustomError>;
}

Example only client

#[rpc(client)]
pub trait Rpc
{
	// I can enter whatever type I want here but it will return ClientError anyway.....
	async fn f(&self) -> Result<String, ClientError>;
}

I mostly care about having the client and server to share the API definition which I like to avoid a bunch of boiler plate and so on.

Copy link
Contributor Author

@MOZGIII MOZGIII Jan 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking about it more like this:

struct CustomError;

#[rpc(server, client)]
pub trait Rpc {
	// The client will return `ClientError` here.....
	async fn f(&self) -> Result<String, CustomError>;
}


struct Server;

#[async_trait]
impl RpcServer for Server {
    async fn f(&self) -> Result<String, ServerError<CustomError>>; // <------ see the `ServerError`
}

And the client would have a ClientError or ClientError<CustomError> type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that idea is interesting but it complicates things in other crates such as core but I would prefer to keep it out of this PR.

That alone would cause plenty of changes

The benefit of splitting it up is that is easier to reason about the error scenarios but forces users to import a few different error types (fine though)

&mut self,
method_name: &'static str,
callback: Fun,
) -> Result<MethodResourcesBuilder, Error>
where
R: Serialize + Send + Sync + 'static,
Fut: Future<Output = Result<R, Error>> + Send,
E: for<'a> Into<Error>,
MOZGIII marked this conversation as resolved.
Show resolved Hide resolved
Fut: Future<Output = Result<R, E>> + Send,
Fun: (Fn(Params<'static>, Arc<Context>) -> Fut) + Clone + Send + Sync + 'static,
{
let ctx = self.ctx.clone();
Expand All @@ -589,7 +590,7 @@ impl<Context: Send + Sync + 'static> RpcModule<Context> {
let future = async move {
let result = match callback(params, ctx).await {
Ok(res) => MethodResponse::response(id, res, max_response_size),
Err(err) => MethodResponse::error(id, err),
Err(err) => MethodResponse::error(id, err.into()),
};

// Release claimed resources
Expand All @@ -606,15 +607,16 @@ impl<Context: Send + Sync + 'static> RpcModule<Context> {

/// Register a new **blocking** synchronous RPC method, which computes the response with the given callback.
/// Unlike the regular [`register_method`](RpcModule::register_method), this method can block its thread and perform expensive computations.
pub fn register_blocking_method<R, F>(
pub fn register_blocking_method<R, E, F>(
&mut self,
method_name: &'static str,
callback: F,
) -> Result<MethodResourcesBuilder, Error>
where
Context: Send + Sync + 'static,
R: Serialize,
F: Fn(Params, Arc<Context>) -> Result<R, Error> + Clone + Send + Sync + 'static,
E: for<'a> Into<Error>,
MOZGIII marked this conversation as resolved.
Show resolved Hide resolved
F: Fn(Params, Arc<Context>) -> Result<R, E> + Clone + Send + Sync + 'static,
{
let ctx = self.ctx.clone();
let callback = self.methods.verify_and_insert(
Expand All @@ -626,7 +628,7 @@ impl<Context: Send + Sync + 'static> RpcModule<Context> {
tokio::task::spawn_blocking(move || {
let result = match callback(params, ctx) {
Ok(result) => MethodResponse::response(id, result, max_response_size),
Err(err) => MethodResponse::error(id, err),
Err(err) => MethodResponse::error(id, err.into()),
};

// Release claimed resources
Expand Down
3 changes: 2 additions & 1 deletion examples/examples/tokio_console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

use std::net::SocketAddr;

use jsonrpsee::core::Error;
use jsonrpsee::server::ServerBuilder;
use jsonrpsee::RpcModule;

Expand All @@ -55,7 +56,7 @@ async fn run_server() -> anyhow::Result<SocketAddr> {
module.register_method("memory_call", |_, _| Ok("A".repeat(1024 * 1024)))?;
module.register_async_method("sleep", |_, _| async {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Ok("lo")
Result::<_, Error>::Ok("lo")
})?;

let addr = server.local_addr()?;
Expand Down
1 change: 1 addition & 0 deletions proc-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ trybuild = "1.0"
tokio = { version = "1.16", features = ["rt", "macros"] }
futures-channel = { version = "0.3.14", default-features = false }
futures-util = { version = "0.3.14", default-features = false }
serde_json = "1"
33 changes: 32 additions & 1 deletion proc-macros/src/render_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use crate::helpers::generate_where_clause;
use crate::rpc_macro::{RpcDescription, RpcMethod, RpcSubscription};
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{FnArg, Pat, PatIdent, PatType, TypeParam};
use syn::{AngleBracketedGenericArguments, FnArg, GenericArgument, Pat, PatIdent, PatType, PathArguments, TypeParam};

impl RpcDescription {
pub(super) fn render_client(&self) -> Result<TokenStream2, syn::Error> {
Expand Down Expand Up @@ -68,6 +68,36 @@ impl RpcDescription {
Ok(trait_impl)
}

fn patch_result_error(&self, ty: &syn::Type) -> syn::Type {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a play and wonder whether this would be a clearer approach to take:

fn return_result_type(&self, ty: &syn::Type) -> TokenStream2 {
	// We expect a valid type path.
	let syn::Type::Path(type_path) = ty else  {
		return quote_spanned!(ty.span() => compile_error!("Expecting something like 'Result<Foo, Err>' here. (1)"));
	};

	// The path (eg std::result::Result) should have a final segment like 'Result'.
	let Some(type_name) = type_path.path.segments.last() else {
		return quote_spanned!(ty.span() => compile_error!("Expecting this path to end in something like 'Result<Foo, Err>'"));
	};

	// Get the generic args eg the <T, E> in Result<T, E>.
	let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = &type_name.arguments else {
		return quote_spanned!(ty.span() => compile_error!("Expecting something like 'Result<Foo, Err>' here, but got no generic args (eg no '<Foo,Err>')."));
	};

	if type_name.ident == "Result"{
		// Result<T, E> should have 2 generic args.
		if args.len() != 2 {
			return quote_spanned!(args.span() => compile_error!("Expecting two generic args here."));
		}
	} else if type_name.ident == "RpcResult" {
		// RpcResult<T> (an alias we export) should have 1 generic arg.
		if args.len() != 1 {
			return quote_spanned!(args.span() => compile_error!("Expecting two generic args here."));
		}
	} else {
		// Any other type name isn't allowed.
		return quote_spanned!(type_name.span() => compile_error!("The response type should be Result or RpcResult"));
	}

	// The first generic arg is our custom return type.
	// Return a new Result with that custom type and jsonrpsee::core::Error:
	let custom_return_type = args.first().unwrap();
	let error_type = self.jrps_client_item(quote! { core::Error });
	quote!(std::result::Result<#custom_return_type, #error_type>)
}

Aside from being documented, it checks for both Result and RpcResult (and verified the number of generics), and returns nicer compile errors which highlight the offending return type when it's wrong.

Instead of modifying the type I just looked for the generic param and then return a new Result type which uses it and the standard error.

Needs testing properly though but it compiles ok!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it is a different approach; when choosing between the two I figured I'd rather impose as few restrictions as possible, and thus tried to surgically target the Result's error. My rationale for going this way is, for instance, I'm not certain at all that types other than RpcResult and Result<_, _> are not allowed... But after a quick look through the tests, I can tell it is the case.
So, yeah, there is definitely no such restriction explicitly anywhere just yet, and I wasn't sure it would be the right way of introducing it.

There's also a concern with supporting multiple paths to [RpcResult] - but I've implemented a nice helper for handling paths for [Result] that would work with [RpcResult] too.

Copy link
Contributor Author

@MOZGIII MOZGIII Jan 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried it! One unexpected artifact is this UI warning:

┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
warning: unused import: `core::RpcResult`
 --> .../jsonrpsee/proc-macros/tests/ui/correct/only_client.rs:3:17
  |
3 | use jsonrpsee::{core::RpcResult, proc_macros::rpc};
  |                 ^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

This comes from this code:

//! Example of using proc macro to generate working client and server.

use jsonrpsee::{core::RpcResult, proc_macros::rpc};

#[rpc(client)]
pub trait Rpc {
	#[method(name = "foo")]
	async fn async_method(&self, param_a: u8, param_b: String) -> RpcResult<u16>;

	#[method(name = "bar")]
	fn sync_method(&self) -> RpcResult<u16>;

	#[subscription(name = "subscribe", item = String)]
	fn sub(&self);
}

fn main() {}

If we rewrite the code to use Result explicitly it might actually make RpcResult unused! Good thing there are UI tests in this project, here...


We can easily handle it by passing through the RpcResult.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if user has a custom Result type for whatever reason? Shall we replace it with std::result::Result? It is a pretty edge case, yet I'd suggest that we pass through that type too, and just alter the last argument. That would be a bit less surprising for someone injecting custom Results.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have a version that covers all concerns now, please take a look. It does error rewrite again to be more permissive but provides those nice compile errors (for which I need to add tests actually!).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if user has a custom Result type for whatever reason? Shall we replace it with std::result::Result? It is a pretty edge case, yet I'd suggest that we pass through that type too, and just alter the last argument. That would be a bit less surprising for someone injecting custom Results.

I think that is the user has some custom type that isn't something we expect (ie a Result or an RpcResult) then offhand the safest option is to complain, since in the general case we can't possibly know what the structure of said custom type is. @niklasad1 do you have any opinions on this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI I had a look at your latest version; it doesn't look any more permissive to me since it still requires the name to be Result or RpcResult (and the Result type still requires 2 args, so it's highly unlikely somebody will be importing a "Result" type alias of that kind anyway). So I still prefer my approach (and avoiding mutating things for clarity) but I'm not super bothered.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is the user has some custom type that isn't something we expect (ie a Result or an RpcResult) then offhand the safest option is to complain, since in the general case we can't possibly know what the structure of said custom type is. @niklasad1 do you have any opinions on this?

Yeah, I agree I think it's better to throw a compile-time error if the error type isn't Result/RpcResult.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The case where this solution is more permissive is when the user provides a type called Result that has the same shape as the std Result, but is a different type. For instance, a struct instead of enum (a case that is unlikely to happen), or a wrapper (much more common, although maybe not for Result), for instance for implementing some traits on it that conflict otherwise.

let mut path = match ty {
syn::Type::Path(path) => path.clone(),
_ => panic!("Client only supports bare or Result values: {:?}", ty),
};

let Some(first_segment) = path.path.segments.first_mut() else {
return syn::Type::Path(path);
};

if first_segment.ident != "Result" {
MOZGIII marked this conversation as resolved.
Show resolved Hide resolved
return syn::Type::Path(path);
}

let args = match first_segment.arguments {
PathArguments::AngleBracketed(AngleBracketedGenericArguments { ref mut args, .. }) => args,
_ => unreachable!("Unexpected Result structure"),
};

let error = args.last_mut().unwrap();
MOZGIII marked this conversation as resolved.
Show resolved Hide resolved
let error_type = match error {
GenericArgument::Type(error_type) => error_type,
_ => unreachable!("Unexpected Result structure"),
};

*error_type = syn::Type::Verbatim(self.jrps_client_item(quote! { core::Error }));

syn::Type::Path(path)
}

fn render_method(&self, method: &RpcMethod) -> Result<TokenStream2, syn::Error> {
// `jsonrpsee::Error`
let jrps_error = self.jrps_client_item(quote! { core::Error });
Expand All @@ -83,6 +113,7 @@ impl RpcDescription {
// `returns` represent the return type of the *rust method* (`Result< <..>, jsonrpsee::core::Error`).
let (called_method, returns) = if let Some(returns) = &method.returns {
let called_method = quote::format_ident!("request");
let returns = self.patch_result_error(returns);
MOZGIII marked this conversation as resolved.
Show resolved Hide resolved
let returns = quote! { #returns };

(called_method, returns)
Expand Down
88 changes: 88 additions & 0 deletions proc-macros/tests/ui/correct/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//! Example of using custom errors.

use std::net::SocketAddr;

use jsonrpsee::core::async_trait;
use jsonrpsee::proc_macros::rpc;
use jsonrpsee::server::ServerBuilder;
use jsonrpsee::ws_client::*;

pub enum CustomError {
One,
Two { custom_data: u32 },
}

impl From<CustomError> for jsonrpsee::core::Error {
fn from(err: CustomError) -> Self {
let code = match &err {
CustomError::One => 101,
CustomError::Two { .. } => 102,
};
let data = match &err {
CustomError::One => None,
CustomError::Two { custom_data } => Some(serde_json::json!({ "customData": custom_data })),
};

let data = data.map(|val| serde_json::value::to_raw_value(&val).unwrap());

let error_object = jsonrpsee::types::ErrorObjectOwned::owned(code, "custom_error", data);

Self::Call(jsonrpsee::types::error::CallError::Custom(error_object))
}
}

#[rpc(client, server, namespace = "foo")]
pub trait Rpc {
#[method(name = "method1")]
async fn method1(&self) -> Result<u16, CustomError>;

#[method(name = "method2")]
async fn method2(&self) -> Result<u16, CustomError>;
}

pub struct RpcServerImpl;

#[async_trait]
impl RpcServer for RpcServerImpl {
async fn method1(&self) -> Result<u16, CustomError> {
Err(CustomError::One)
}

async fn method2(&self) -> Result<u16, CustomError> {
Err(CustomError::Two { custom_data: 123 })
}
}

pub async fn server() -> SocketAddr {
let server = ServerBuilder::default().build("127.0.0.1:0").await.unwrap();
let addr = server.local_addr().unwrap();
let server_handle = server.start(RpcServerImpl.into_rpc()).unwrap();

tokio::spawn(server_handle.stopped());

addr
}

#[tokio::main]
async fn main() {
let server_addr = server().await;
let server_url = format!("ws://{}", server_addr);
let client = WsClientBuilder::default().build(&server_url).await.unwrap();

let get_error_object = |err| match err {
jsonrpsee::core::Error::Call(jsonrpsee::types::error::CallError::Custom(object)) => object,
_ => panic!("wrong error kind: {:?}", err),
};

let error = client.method1().await.unwrap_err();
let error_object = get_error_object(error);
assert_eq!(error_object.code(), 101);
assert_eq!(error_object.message(), "custom_error");
assert!(error_object.data().is_none());

let error = client.method2().await.unwrap_err();
let error_object = get_error_object(error);
assert_eq!(error_object.code(), 102);
assert_eq!(error_object.message(), "custom_error");
assert_eq!(error_object.data().unwrap().get(), r#"{"customData":123}"#);
}
10 changes: 5 additions & 5 deletions server/src/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ pub(crate) async fn server_with_handles() -> (SocketAddr, ServerHandle) {
tracing::debug!("server respond to hello");
// Call some async function inside.
futures_util::future::ready(()).await;
Ok("hello")
Result::<_, Error>::Ok("hello")
}
})
.unwrap();
module
.register_async_method("add_async", |params, _| async move {
let params: Vec<u64> = params.parse()?;
let sum: u64 = params.into_iter().sum();
Ok(sum)
Result::<_, Error>::Ok(sum)
})
.unwrap();
module
Expand Down Expand Up @@ -111,7 +111,7 @@ pub(crate) async fn server_with_handles() -> (SocketAddr, ServerHandle) {
module
.register_async_method("should_ok_async", |_p, ctx| async move {
ctx.ok().map_err(CallError::Failed)?;
Ok("ok")
Result::<_, Error>::Ok("ok")
})
.unwrap();

Expand Down Expand Up @@ -146,15 +146,15 @@ pub(crate) async fn server_with_context() -> SocketAddr {
.register_async_method("should_ok_async", |_p, ctx| async move {
ctx.ok().map_err(CallError::Failed)?;
// Call some async function inside.
Ok(futures_util::future::ready("ok!").await)
Result::<_, Error>::Ok(futures_util::future::ready("ok!").await)
})
.unwrap();

rpc_module
.register_async_method("err_async", |_p, ctx| async move {
ctx.ok().map_err(CallError::Failed)?;
// Async work that returns an error
futures_util::future::err::<(), _>(anyhow!("nah").into()).await
futures_util::future::err::<(), Error>(anyhow!("nah").into()).await
})
.unwrap();

Expand Down
4 changes: 2 additions & 2 deletions server/src/tests/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async fn server() -> (SocketAddr, ServerHandle) {
let mut module = RpcModule::new(ctx);
let addr = server.local_addr().unwrap();
module.register_method("say_hello", |_, _| Ok("lo")).unwrap();
module.register_async_method("say_hello_async", |_, _| async move { Ok("lo") }).unwrap();
module.register_async_method("say_hello_async", |_, _| async move { Result::<_, Error>::Ok("lo") }).unwrap();
module
.register_method("add", |params, _| {
let params: Vec<u64> = params.parse()?;
Expand Down Expand Up @@ -78,7 +78,7 @@ async fn server() -> (SocketAddr, ServerHandle) {
module
.register_async_method("should_ok_async", |_p, ctx| async move {
ctx.ok().map_err(CallError::Failed)?;
Ok("ok")
Result::<_, Error>::Ok("ok")
})
.unwrap();

Expand Down
2 changes: 1 addition & 1 deletion tests/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ pub async fn server() -> SocketAddr {
module
.register_async_method("slow_hello", |_, _| async {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok("hello")
Result::<_, Error>::Ok("hello")
})
.unwrap();

Expand Down
6 changes: 3 additions & 3 deletions tests/tests/resource_limiting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,20 @@ fn module_manual() -> Result<RpcModule<()>, Error> {

module.register_async_method("say_hello", |_, _| async move {
sleep(Duration::from_millis(50)).await;
Ok("hello")
Result::<_, Error>::Ok("hello")
})?;

module
.register_async_method("expensive_call", |_, _| async move {
sleep(Duration::from_millis(50)).await;
Ok("hello expensive call")
Result::<_, Error>::Ok("hello expensive call")
})?
.resource("CPU", 3)?;

module
.register_async_method("memory_hog", |_, _| async move {
sleep(Duration::from_millis(50)).await;
Ok("hello memory hog")
Result::<_, Error>::Ok("hello memory hog")
})?
.resource("CPU", 0)?
.resource("MEM", 8)?;
Expand Down
2 changes: 1 addition & 1 deletion tests/tests/rpc_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ async fn calling_method_without_server() {
module
.register_async_method("roo", |params, ctx| {
let ns: Vec<u8> = params.parse().expect("valid params please");
async move { Ok(ctx.roo(ns)) }
async move { Result::<_, Error>::Ok(ctx.roo(ns)) }
})
.unwrap();
let res: u64 = module.call("roo", [12, 13]).await.unwrap();
Expand Down