4.3 Send and recv

Overview

Send and recv are used in executing low-level operations. Deno has more than 100 low-level operations, and the list keeps increasing. Instead of registering each low-level operation, Deno registers a wrapper called send.
Send function is used to take the bridge from v8 to Deno. Similarly, recv function is used to save reference to a callback function that would be useful to take the bridge to go back to v8.

Functions

Registration

The registration of these function happens at the startup:
v8::ExternalReference {
function: recv.map_fn_to()
},
v8::ExternalReference {
function: send.map_fn_to()
},

JS space

Unlike console.log, send and recv are very basic infrastructure functions useful in routing ops and processing response. In chapter 6, we'll see more about how ops are routed through JS to Rust. This is a bit too early to go into ops routing on the JS side. We'll limit ourselves to the JS function that calls these functions.
Send is called from a core function dispatch:
function dispatch(opName, control, ...zeroCopy) {
return send(opsCache[opName], control, ...zeroCopy);
}
Send function is called for every op dispatch i.e. send function gets called one time for every low-level op.
Recv is only called during initialization with a callback:
recv(handleAsyncMsgFromRust);
Recv is called only one, and the purpose of recv is to save the reference of the callback function.

Rust space

Both send and recv functions are part of Deno. They're implemented in Rust.

Send

Here is the code for the send function:
fn send<'s>(
scope: &mut v8::HandleScope<'s>,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue,
) {
let state_rc = JsRuntime::state(scope);
let state = state_rc.borrow_mut();
let op_id = match v8::Local::<v8::Integer>::try_from(args.get(0))
.map_err(AnyError::from)
.and_then(|l| OpId::try_from(l.value()).map_err(AnyError::from))
{
Ok(op_id) => {
debug!("send id={}", op_id);
op_id},
Err(err) => {
let msg = format!("invalid op id: {}", err);
let msg = v8::String::new(scope, &msg).unwrap();
let exc = v8::Exception::type_error(scope, msg);
scope.throw_exception(exc);
return;
}
};
let buf_iter = (1..args.length()).map(|idx| {
v8::Local::<v8::ArrayBufferView>::try_from(args.get(idx))
.map(|view| ZeroCopyBuf::new(scope, view))
.map_err(|err| {
let msg = format!("Invalid argument at position {}: {}", idx, err);
let msg = v8::String::new(scope, &msg).unwrap();
v8::Exception::type_error(scope, msg)
})
});
let bufs = match buf_iter.collect::<Result<_, _>>() {
Ok(bufs) => bufs,
Err(exc) => {
scope.throw_exception(exc);
return;
}
};
let op = OpTable::route_op(op_id, state.op_state.clone(), bufs);
assert_eq!(state.shared.size(), 0);
match op {
Op::Sync(buf) if !buf.is_empty() => {
rv.set(boxed_slice_to_uint8array(scope, buf).into());
}
Op::Sync(_) => {}
Op::Async(fut) => {
let fut2 = fut.map(move |buf| (op_id, buf));
state.pending_ops.push(fut2.boxed_local());
state.have_unpolled_ops.set(true);
}
Op::AsyncUnref(fut) => {
let fut2 = fut.map(move |buf| (op_id, buf));
state.pending_unref_ops.push(fut2.boxed_local());
state.have_unpolled_ops.set(true);
}
Op::NotFound => {
let msg = format!("Unknown op id: {}", op_id);
let msg = v8::String::new(scope, &msg).unwrap();
let exc = v8::Exception::type_error(scope, msg);
scope.throw_exception(exc);
}
}
}
It'd make more sense to understand send function when ops are discussed in chapter 6. For now, we'll just list down the steps briefly:
  • Get op id (this came from JS space)
  • Route op
  • Process op response
    • Sync op: return result immediately
    • Async op: save future for polling
If it's an async op, the response would go later. Recv is one of the ways to send asynchronous responses later. The other more commonly used way is the shared queue. We'll see the shared queue shortly.

Recv

Here is the code for the recv function:
fn recv(
scope: &mut v8::HandleScope,
args: v8::FunctionCallbackArguments,
_rv: v8::ReturnValue,
) {
let state_rc = JsRuntime::state(scope);
let mut state = state_rc.borrow_mut();
let cb = match v8::Local::<v8::Function>::try_from(args.get(0)) {
Ok(cb) => cb,
Err(err) => return throw_type_error(scope, err.to_string()),
};
let slot = match &mut state.js_recv_cb {
slot @ None => slot,
_ => return throw_type_error(scope, "Deno.core.recv() already called"),
};
slot.replace(v8::Global::new(scope, cb));
}
Unlike it's name, the recv function is not the reverse of the send function. The purpose of recv function is to save the reference of the callback into the state. This callback would be called whenever there is a response to be sent to V8/JS.