4.4 Encode and decode
Encode and decode are another set of infrastructure functions, just like send and recv. In fact, most of the external references are for infrastructure work. The only exceptions are print, get promise details, etc.
Encode and decode are used to establish a protocol between v8 and Deno. Both encode and decode are called from v8.
Encode is used to encode a string into bytes before crossing the bridge from v8 to Deno. Encode is called before send.
Decode it used to decode bytes into a string after crossing the bridge from Deno to v8. Decode is called after recv or shared queue.

The registration of these function happens at the startup:
v8::ExternalReference {
function: encode.map_fn_to()
},
v8::ExternalReference {
function: decode.map_fn_to()
},
Encode function gets called before calling send function to invoke a low-level op. Both encode and send are external references for v8. V8 calls them in the following order:
- encode
- send
Encode is used for both async and sync ops, but we'll only see one as an example:
function jsonOpSync(opName, args = {}, ...zeroCopy) {
const argsBuf = encodeJson(args);
const res = dispatch(opName, argsBuf, ...zeroCopy);
return processResponse(decodeJson(res));
}
function encodeJson(args) {
const s = JSON.stringify(args);
return core.encode(s);
}
function decodeJson(ui8) {
const s = core.decode(ui8);
return JSON.parse(s);
}
encodeJson first stringifies the object and then calls encode to convert it to bytes. Once encoding is done, dispatch gets called which calls send.
Similarly, decode is called after a response is received from Rust. decodeJson first converts bytes to string and then parses string into an object.
Let's see the code of the encode function:
fn encode(
scope: &mut v8::HandleScope,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue,
) {
let text = match v8::Local::<v8::String>::try_from(args.get(0)) {
Ok(s) => s,
Err(_) => {
let msg = v8::String::new(scope, "Invalid argument").unwrap();
let exception = v8::Exception::type_error(scope, msg);
scope.throw_exception(exception);
return;
}
};
let text_str = text.to_rust_string_lossy(scope);
let text_bytes = text_str.as_bytes().to_vec().into_boxed_slice();
// -- CODE OMITTED --
rv.set(buf.into())
}
The function is big, but it carries out a simple task:
- Convert string to bytes
- Set the return value (rv) so that v8 can proceed after the encode returns
Here is an example of an object and the encoded bytes:
// OBJECT TO ENCODE
{ path: "/var/tmp/test.log", len: 0, promiseId: 2 }
// ENCODED BYTES
[
123, 34, 112, 97, 116, 104, 34, 58, 34, 47,
118, 97, 114, 47, 116, 109, 112, 47, 116, 101,
115, 116, 46, 108, 111, 103, 34, 44, 34, 108,
101, 110, 34, 58, 48, 44, 34, 112, 114, 111,
109, 105, 115, 101, 73, 100, 34, 58, 50, 125
]
The code for decode is also equally simple:
fn decode(
scope: &mut v8::HandleScope,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue,
) {
let view = match v8::Local::<v8::ArrayBufferView>::try_from(args.get(0)) {
Ok(view) => view,
Err(_) => {
let msg = v8::String::new(scope, "Invalid argument").unwrap();
let exception = v8::Exception::type_error(scope, msg);
scope.throw_exception(exception);
return;
}
};
let backing_store = view.buffer(scope).unwrap().get_backing_store();
let buf = unsafe {
get_backing_store_slice(
&backing_store,
view.byte_offset(),
view.byte_length(),
)
};
match v8::String::new_from_utf8(scope, &buf, v8::NewStringType::Normal) {
Some(text) => rv.set(text.into()),
None => {
let msg = v8::String::new(scope, "string too long").unwrap();
let exception = v8::Exception::range_error(scope, msg);
scope.throw_exception(exception);
}
};
}
Decode is also big, but it is equally simple:
- Get buffer
- Convert buffer to string
- Set return value so that v8 can proceed after the decode returns
Here is an example of bytes and the decode object:
// BYTES
[
123, 34, 111, 107, 34, 58,
123, 125, 44, 34, 112, 114,
111, 109, 105, 115, 101, 73,
100, 34, 58, 50, 125
]
// DECODED OBJECT
{ ok: {}, promiseId: 2 }