Skip to content

Commit

Permalink
feat: add prim call_raw : (Principal, Text, Blob) -> async Blob (#3086)
Browse files Browse the repository at this point in the history
Fixes #2703 (by lowering ourselves to the level of Rust).

Adds a prim to dynamically invoke a method by name with an already serialized blob and get the raw (undeserialized) blob back asynchronously:

``` Motoko
call_raw : (canister : Principal, function_name : Text, arg : Blob) -> async Blob
````

The function can only be called in an asynchronous context and this is enforced by the type system.

There is no assumption that the contents of either blob is Candid, so this could also be used to talk to non-Candid endpoints.

This should be sufficient to implement the call-forwarding functionality of the Rust cycles wallet.


- [x] Determine whether the method name must be (rope)-normalized before use. Currently it is not. @nomeata,  what's the representation invariant for ordinary shared functions - I see they are (Principal,Text) pairs, but is the Text normalized? 
- [ ] Any ideas for better name: `request`, `send`, `call`, `invoke`, `call_dynamic` spring to mind
  • Loading branch information
crusso authored Jan 30, 2022
1 parent 5f678e9 commit 1c4e18e
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 12 deletions.
53 changes: 47 additions & 6 deletions src/codegen/compile.ml
Original file line number Diff line number Diff line change
Expand Up @@ -6362,8 +6362,22 @@ module FuncDec = struct
deserialization); the reject callback function is unique.
*)

let closures_to_reply_reject_callbacks env ts =
let reply_name = "@callback<" ^ Typ_hash.typ_hash (Type.Tup ts) ^ ">" in
let closures_to_reply_reject_callbacks_aux env ts_opt =
let arity, reply_name, from_arg_data =
match ts_opt with
| Some ts ->
(List.length ts,
"@callback<" ^ Typ_hash.typ_hash (Type.Tup ts) ^ ">",
fun env -> Serialization.deserialize env ts)
| None ->
(1,
"@callback",
(fun env ->
Blob.of_size_copy env
(fun env -> IC.system_call env "ic0" "msg_arg_data_size")
(fun env -> IC.system_call env "ic0" "msg_arg_data_copy")
(fun env -> compile_unboxed_const 0l)))
in
Func.define_built_in env reply_name ["env", I32Type] [] (fun env ->
message_start env (Type.Shared Type.Write) ^^
(* Look up continuation *)
Expand All @@ -6374,11 +6388,11 @@ module FuncDec = struct
set_closure ^^
get_closure ^^

(* Deserialize reply arguments *)
Serialization.deserialize env ts ^^
(* Deserialize/Blobify reply arguments *)
from_arg_data env ^^

get_closure ^^
Closure.call_closure env (List.length ts) 0 ^^
Closure.call_closure env arity 0 ^^

message_cleanup env (Type.Shared Type.Write)
);
Expand Down Expand Up @@ -6418,6 +6432,11 @@ module FuncDec = struct
compile_unboxed_const (E.add_fun_ptr env (E.built_in env reject_name)) ^^
get_cb_index

let closures_to_reply_reject_callbacks env ts =
closures_to_reply_reject_callbacks_aux env (Some ts)
let closures_to_raw_reply_reject_callbacks env =
closures_to_reply_reject_callbacks_aux env None

let ignoring_callback env =
(* for one-way calls, we use an invalid table entry as the callback. this
way, the callback, when it comes back, will (safely) trap, even if the
Expand Down Expand Up @@ -6470,6 +6489,14 @@ module FuncDec = struct
(closures_to_reply_reject_callbacks env ts2 [get_k; get_r])
(fun _ -> get_arg ^^ Serialization.serialize env ts1)

let ic_call_raw env get_meth_pair get_arg get_k get_r =
ic_call_threaded
env
"raw call"
get_meth_pair
(closures_to_raw_reply_reject_callbacks env [get_k; get_r])
(fun _ -> get_arg ^^ Blob.as_ptr_len env)

let ic_self_call env ts get_meth_pair get_future get_k get_r =
ic_call_threaded
env
Expand Down Expand Up @@ -8234,7 +8261,21 @@ and compile_exp (env : E.t) ae exp =
compile_exp_vanilla env ae r ^^ set_r ^^
FuncDec.ic_call env ts1 ts2 get_meth_pair get_arg get_k get_r add_cycles
end

| ICCallRawPrim, [p;m;a;k;r] ->
SR.unit, begin
let (set_meth_pair, get_meth_pair) = new_local env "meth_pair" in
let (set_arg, get_arg) = new_local env "arg" in
let (set_k, get_k) = new_local env "k" in
let (set_r, get_r) = new_local env "r" in
let add_cycles = Internals.add_cycles env ae in
compile_exp_vanilla env ae p ^^
compile_exp_vanilla env ae m ^^ Text.to_blob env ^^
Tuple.from_stack env 2 ^^ set_meth_pair ^^
compile_exp_vanilla env ae a ^^ set_arg ^^
compile_exp_vanilla env ae k ^^ set_k ^^
compile_exp_vanilla env ae r ^^ set_r ^^
FuncDec.ic_call_raw env get_meth_pair get_arg get_k get_r add_cycles
end
| ICStableRead ty, [] ->
(*
* On initial install:
Expand Down
1 change: 1 addition & 0 deletions src/ir_def/arrange_ir.ml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ and prim = function
| ICRejectPrim -> Atom "ICRejectPrim"
| ICCallerPrim -> Atom "ICCallerPrim"
| ICCallPrim -> Atom "ICCallPrim"
| ICCallRawPrim -> Atom "ICCallRawPrim"
| ICStableWrite t -> "ICStableWrite" $$ [typ t]
| ICStableRead t -> "ICStableRead" $$ [typ t]

Expand Down
8 changes: 8 additions & 0 deletions src/ir_def/check_ir.ml
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,14 @@ let rec check_exp env (exp:Ir.exp) : unit =
error env exp1.at "expected function type, but expression produces type\n %s"
(T.string_of_typ_expand t1)
end
(* TODO: T.unit <: t ? *)
| ICCallRawPrim, [exp1; exp2; exp3; k; r] ->
typ exp1 <: T.principal;
typ exp2 <: T.text;
typ exp3 <: T.blob;
typ k <: T.Func (T.Local, T.Returns, [], [T.blob], []);
typ r <: T.Func (T.Local, T.Returns, [], [T.error], []);
T.unit <: t
| ICStableRead t1, [] ->
check_typ env t1;
check (store_typ t1) "Invalid type argument to ICStableRead";
Expand Down
8 changes: 8 additions & 0 deletions src/ir_def/construct.ml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ let ic_callE f e k r =
note = Note.{ def with typ = T.unit; eff = eff }
}

let ic_call_rawE p m a k r =
let es = [p; m; a; k; r] in
let effs = List.map eff es in
let eff = List.fold_left max_eff T.Triv effs in
{ it = PrimE (ICCallRawPrim, es);
at = no_region;
note = Note.{ def with typ = T.unit; eff = eff }
}

(* tuples *)

Expand Down
1 change: 1 addition & 0 deletions src/ir_def/construct.mli
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ val cps_awaitE : typ -> exp -> exp -> exp
val ic_replyE : typ list -> exp -> exp
val ic_rejectE : exp -> exp
val ic_callE : exp -> exp -> exp -> exp -> exp
val ic_call_rawE : exp -> exp -> exp -> exp -> exp -> exp
val projE : exp -> int -> exp
val optE : exp -> exp
val tagE : id -> exp -> exp
Expand Down
4 changes: 3 additions & 1 deletion src/ir_def/ir.ml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ and prim =
| ICRejectPrim
| ICCallerPrim
| ICCallPrim
| ICCallRawPrim
| ICStableWrite of Type.typ (* serialize value of stable type to stable memory *)
| ICStableRead of Type.typ (* deserialize value of stable type from stable memory *)

Expand Down Expand Up @@ -294,7 +295,8 @@ let map_prim t_typ t_id p =
| ICReplyPrim ts -> ICReplyPrim (List.map t_typ ts)
| ICRejectPrim
| ICCallerPrim
| ICCallPrim -> p
| ICCallPrim
| ICCallRawPrim -> p
| ICStableWrite t -> ICStableWrite (t_typ t)
| ICStableRead t -> ICStableRead (t_typ t)

17 changes: 17 additions & 0 deletions src/ir_passes/async.ml
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,23 @@ let transform mode prog =
)
(varE nary_async))
.it
| PrimE (OtherPrim "call_raw", [exp1; exp2; exp3]) ->
let exp1' = t_exp exp1 in
let exp2' = t_exp exp2 in
let exp3' = t_exp exp3 in
let ((nary_async, nary_reply, reject), def) = new_nary_async_reply mode [T.blob] in
let _ = letEta in
(blockE (
letP (tupP [varP nary_async; varP nary_reply; varP reject]) def ::
letEta exp1' (fun v1 ->
letEta exp2' (fun v2 ->
letEta exp3' (fun v3 ->
[ expD (ic_call_rawE v1 v2 v3 (varE nary_reply) (varE reject)) ]
)
))
)
(varE nary_async))
.it
| PrimE (p, exps) ->
PrimE (t_prim p, List.map t_exp exps)
| BlockE b ->
Expand Down
5 changes: 5 additions & 0 deletions src/prelude/internals.mo
Original file line number Diff line number Diff line change
Expand Up @@ -406,3 +406,8 @@ func @create_actor_helper(wasm_module_ : Blob, arg_ : Blob) : async Principal =
});
return canister_id_;
};
// raw calls
func @call_raw(p : Principal, m : Text, a : Blob) : async Blob {
await (prim "call_raw" : (Principal, Text, Blob) -> async Blob) (p, m, a);
};
2 changes: 2 additions & 0 deletions src/prelude/prim.mo
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,5 @@ func stableMemoryLoadBlob(offset : Nat64, size : Nat) : Blob =
func stableMemoryStoreBlob(offset : Nat64, val : Blob) : () =
(prim "stableMemoryStoreBlob" : (Nat64, Blob) -> ()) (offset, val);
let call_raw = @call_raw;
10 changes: 5 additions & 5 deletions test/fail/ok/illegal-await.tc.ok
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ illegal-await.mo:24.11: info, start of scope $anon-async-24.11 mentioned in erro
illegal-await.mo:26.5: info, end of scope $anon-async-24.11 mentioned in error at illegal-await.mo:25.7-25.14
illegal-await.mo:22.10: info, start of scope $anon-async-22.10 mentioned in error at illegal-await.mo:25.7-25.14
illegal-await.mo:27.3: info, end of scope $anon-async-22.10 mentioned in error at illegal-await.mo:25.7-25.14
illegal-await.mo:35.11-35.12: type error [M0087], ill-scoped await: expected async type from current scope $Rec, found async type from other scope $/3
illegal-await.mo:35.11-35.12: type error [M0087], ill-scoped await: expected async type from current scope $Rec, found async type from other scope $/5
scope $Rec is illegal-await.mo:33.44-40.2
scope $/3 is illegal-await.mo:33.1-40.2
scope $/5 is illegal-await.mo:33.1-40.2
illegal-await.mo:33.44: info, start of scope $Rec mentioned in error at illegal-await.mo:35.5-35.12
illegal-await.mo:40.1: info, end of scope $Rec mentioned in error at illegal-await.mo:35.5-35.12
illegal-await.mo:33.1: info, start of scope $/3 mentioned in error at illegal-await.mo:35.5-35.12
illegal-await.mo:40.1: info, end of scope $/3 mentioned in error at illegal-await.mo:35.5-35.12
illegal-await.mo:33.1: info, start of scope $/5 mentioned in error at illegal-await.mo:35.5-35.12
illegal-await.mo:40.1: info, end of scope $/5 mentioned in error at illegal-await.mo:35.5-35.12
illegal-await.mo:38.20-38.21: type error [M0096], expression of type
async<$/3> ()
async<$/5> ()
cannot produce expected type
async<$Rec> ()
110 changes: 110 additions & 0 deletions test/run-drun/call-raw.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import P "mo:⛔";

actor self {

public shared func sint() : async Int {
return 2;
};

public shared func snat() : async Nat {
return 2;
};

public shared func stext() : async Text {
return "hello";
};

public shared func stuple() : async (Nat, Bool, Char) {
return (1, true, 'a');
};

public shared func unit() : async () {
P.debugPrint("unit!");
};

public shared func int(n : Int) : async Int {
P.debugPrint(debug_show("int",n));
return n;
};

public shared func text(t : Text) : async Text {
P.debugPrint(debug_show("text", t));
return t;
};

public shared func tuple(n: Nat, b: Bool, c: Char) : async (Nat, Bool, Char) {
P.debugPrint(debug_show("text", (n, b, c)));
return (n, b, c);
};

public shared func trapInt(n : Int) : async Int {
P.trap("ohoh");
};

public shared func supercalifragilisticexpialidocious() : async () {
P.debugPrint("supercalifragilisticexpialidocious");
};

public shared func go() : async () {
let p = P.principalOfActor(self);

do {
let arg : Blob = "DIDL\00\00";
let res = await P.call_raw(p,"unit", arg);
assert (res == arg);
};

do {
let arg : Blob = "DIDL\00\01\7c\01";
let res = await P.call_raw(p,"int", arg);
assert (res == arg);
};

do {
let arg : Blob = "DIDL\00\01\7c\02";
let res = await P.call_raw(p,"int", arg);
assert (res == arg);
};

do {
let arg : Blob = "DIDL\00\01\71\05\68\65\6c\6c\6f";
let res = await P.call_raw(p,"text", arg);
assert (res == arg);
};

do {
let arg : Blob = "DIDL\00\03\7d\7e\79\01\01\61\00\00\00";
let res = await P.call_raw(p,"tuple", arg);
assert (res == arg);
};

do {
let arg : Blob = "DIDL\00\01\7c\01";
try {
let res = await P.call_raw(p,"trapInt", arg);
assert false;
}
catch e {
P.debugPrint(P.errorMessage(e));
}
};

do {
let m = "super"#"cali"#"fragilisticexpialidocious";
let arg : Blob = "DIDL\00\00";
let res = await P.call_raw(p, m, arg);
assert (res == arg);
};

}
};

//SKIP run
//SKIP run-low
//SKIP run-ir
//CALL ingress sint 0x4449444C0000
//CALL ingress snat 0x4449444C0000
//CALL ingress stext 0x4449444C0000
//CALL ingress stuple 0x4449444C0000
//CALL ingress go 0x4449444C0000

14 changes: 14 additions & 0 deletions test/run-drun/ok/call-raw.drun-run.ok
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ingress Completed: Reply: 0x4449444c016c01b3c4b1f204680100010a00000000000000000101
ingress Completed: Reply: 0x4449444c0000
ingress Completed: Reply: 0x4449444c00017c02
ingress Completed: Reply: 0x4449444c00017d02
ingress Completed: Reply: 0x4449444c0001710568656c6c6f
ingress Completed: Reply: 0x4449444c00037d7e79010161000000
debug.print: unit!
debug.print: ("int", +1)
debug.print: ("int", +2)
debug.print: ("text", "hello")
debug.print: ("text", (1, true, 'a'))
debug.print: IC0503: Canister rwlgt-iiaaa-aaaaa-aaaaa-cai trapped explicitly: ohoh
debug.print: supercalifragilisticexpialidocious
ingress Completed: Reply: 0x4449444c0000
21 changes: 21 additions & 0 deletions test/run-drun/ok/call-raw.ic-ref-run.ok
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
→ update create_canister(record {settings = null})
← replied: (record {hymijyo = principal "cvccv-qqaaq-aaaaa-aaaaa-c"})
→ update install_code(record {arg = blob ""; kca_xin = blob "\00asm\01\00\00\00\0…
← replied: ()
→ update sint()
← replied: (+2)
→ update snat()
← replied: (2)
→ update stext()
← replied: ("hello")
→ update stuple()
← replied: (1, true, (97 : nat32))
→ update go()
debug.print: unit!
debug.print: ("int", +1)
debug.print: ("int", +2)
debug.print: ("text", "hello")
debug.print: ("text", (1, true, 'a'))
debug.print: canister trapped: EvalTrapError region:0xXXX-0xXXX "canister trapped explicitly: ohoh"
debug.print: supercalifragilisticexpialidocious
← replied: ()

0 comments on commit 1c4e18e

Please sign in to comment.