# Asterisc -- but can it Rust?
> "RISC architecture is going to change everything." - Angelina Jolie with Sade playing in the background (??)
{%youtube yVo9_h3-zwQ%}
---
[`asterisc`][asterisc] is a RISC-V 64 VM written by [@protolambda][protolambda], designed to prove correct execution of a RISC-V program with an interactive fault proof. Following its counterparts in the [OP Stack][op-stack] ecosystem, [`cannon`][cannon] & [`cannon-rs`][cannon-rs], which both emulate the MIPS32rel1 ISA, it's an awesome upgrade. Though `asterisc` was initially designed primarily for supporting a subset of the RISC-V ISA and Linux syscalls that Golang uses, I got curious and wanted to spike out support for Rust programs on it as well.
## State of the World
As it stands, `asterisc` is nearly feature complete, and supports the following extensions of the RISC-V instruction set (pulled directly from the project's `README`):
- `RV32I` support - 32 bit base instruction set
- `FENCE`, `ECALL`, `EBREAK` are hardwired to implement a minimal subset of systemcalls of the linux kernel
- Work in progress. All syscalls used by the Golang `risc64` runtime.
- `RV64I` support
- `RV32M`+`RV64M`: Multiplication support
- `RV32A`+`RV64A`: Atomics support
- `RV{32,64}{D,F,Q}`: no-op: No floating points support (since no IEEE754 determinism with rounding modes etc., nor worth the complexity)
- `Zifencei`: `FENCE.I` no-op: No need for `FENCE.I`
- `Zicsr`: no-op: some support for Control-and-status registers may come later though.
- `Ztso`: no-op: no need for Total Store Ordering
- other: revert with error code on unrecognized instructions
[Proto][protolambda] also left a great deal of documentation around supported syscalls, auxilary vectors used by Golang, etc. Highly recommend [checking them out](https://github.com/protolambda/asterisc/tree/master/docs).
## First Attempts
I spun up a simple hello world program and attempted to compile it down to ELF to run on top of `asterisc` in order to sus out whether or not I had out-of-the-box support. I started with using [`cross`][cross] to compile down to the tier 2 `riscv64gc-unknown-linux-gnu` target.
```rs
fn main() {
println!("hello, asterisc!");
}
```
```shell
$ CROSS_CONTAINER_OPTS="--platform linux/amd64" \
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C target-feature=+crt-static" \
cross build --release --target riscv64gc-unknown-linux-gnu
```
First result, no dice on running on top of `asterisc`. On the third instruction, I received an "unsupported opcode" error. Proto suggested that the instruction that was causing the failure was part of the **RVC** (RISC-V standard compressed instruction set) extension, which Golang does not enable, but Rust does (at least, in the majority of built-in targets.)
> Side bar: [Aesthetically pleasing *and* useful tools = good](https://luplab.gitlab.io/rvcodecjs/#q=45378082&abi=false&isa=AUTO)
### Attempts to disable `C`
Taking a look at the supported target features, we can see the troublesome RVC extension right at the top:
```shell
$ rustc --print target-features --target riscv64gc-unknown-linux-gnu
Features supported by rustc for this target:
a - 'A' (Atomic Instructions).
c - 'C' (Compressed Instructions).
d - 'D' (Double-Precision Floating-Point).
e - Implements RV{32,64}E (provides 16 rather than 32 GPRs).
f - 'F' (Single-Precision Floating-Point).
m - 'M' (Integer Multiplication and Division).
...
```
Because the toolchain also is distributed with this feature enabled (hence `riscv64g**c**-unknown-linux-gnu`), I first tried to instruct `rustc` to disable this target feature, statically link the binary, and also to rebuild the core/std toolchain with the new target feature configuration:
```
CROSS_CONTAINER_OPTS="--platform linux/amd64" \
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C target-feature=-c,+crt-static" \
cross build --release --target riscv64gc-unknown-linux-gnu -Z build-std
```
as well as spin up a custom target JSON + a build image to natively build with [the `riscv-gnu-toolchain`](https://github.com/riscv-collab/riscv-gnu-toolchain), both [to no avail](https://users.rust-lang.org/t/disabling-the-c-extension-for-the-riscv64gc-unknown-linux-gnu-target/103815):
```
$ riscv64-linux-gnu-readelf -e ./target/riscv64gc-unknown-linux-gnu/release/noc
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: RISC-V
Version: 0x1
Entry point address: 0x12fbc
Start of program headers: 64 (bytes into file)
Start of section headers: 1119840 (bytes into file)
# curse you, opinionated compiler devs...
# ||| |||
# vvv vvv
Flags: 0x5, RVC, double-float ABI
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 8
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
...
```
## The `C` extension
Because it is non-trivial to disable the `C` extension without dropping to a lesser-supported Rust target, and I'd prefer if `asterisc` supported more generic Rust programs for accessibility, I opted to learn a little bit about RISC-V and add support for the `C` extension into `asterisc` rather than futz around with `rustc` and `LLVM` any longer. (Note that it *is* trivial to compile down to rv64g, but *not* with the standard library nor a high-tier risc-v target triple.)
The `C` extension defines a set of [c]ompressed instructions, represented by 16 bits rather than 32 bits. FPROX has an excellent substack post[^2] describing the extension at a high level, and the reference pdf[^1] contains a more thorough specification.
### Decompressing `c` extension instructions
The RISC-V base ISA (RV32I / RV64I) defines 32-bit wide instructions, which are complimented by the `C` extension with compressed 16-bit instructions. After a quick exploration of some existing RISC-V VMs, there seemed to be two popular approaches for supporting the `c` extension:
1. Interpret them directly, apply separate handlers.
2. Decompress them and interpret as 32-bit analogue instructions.
Because the `C` extension is not necessary for the default Golang RISC-V target, and I wanted to ensure that the change wasn't too invasive, I opted for the second approach.
Encoding for the compressed instructions is as follows[^1]:
<center>
<img src="https://hackmd.io/_uploads/r1-NFRDUT.png">
</center>
Generally, each of the compressed instructions map relatively cleanly to 32-bit counterparts. As a visual example (stolen from FPROX's post[^2]), the CI type instructions map directly to I-types:
<center>
<img src="https://hackmd.io/_uploads/ryK1nxdLT.png">
</center>
<br>
To begin, we need to determine whether or not an instruction is compressed. We can do this by checking that the the low-order 2 bits are set. The RISC-V ISA reserves the low-order instruction bits `00`, `01`, and `10` for the `C` extension[^1], so we can return early with the original 32-bit instruction if both of these bits are set.
For the 3 operations supported by RV64gc, we have:
> 📝 **Note:** The "supported" column signals operations that `asterisc` plans to support. Unsupported `C` extension operations are mostly floating point operations.
**`C0`**
| Function | Format | Funct3 Bits | Supported |
| ---------------------------------- | ------ | ----------- | --------- |
| `C.ADDI4SPN / Illegal instruction` | `CIW` | `000` | ✅ |
| `C.FLD` | `CL` | `001` | ❌ |
| `C.LW` | `CL` | `010` | ✅ |
| `C.LD` | `CL` | `011` | ✅ |
| `Reserved` | `~` | `100` | `~` |
| `C.FSD` | `CS` | `101` | ❌ |
| `C.SW` | `CS` | `110` | ✅ |
| `C.SD` | `CS` | `111` | ✅ |
**`C1`**
| Function | Format | Funct3 Bits | Supported |
| ------------------------------------------------------------------------------- | ------ | ----------- | --------- |
| `C.NOP, C.ADDI` | `CI` | `000` | ✅ |
| `C.ADDIW` | `CI` | `001` | ✅ |
| `C.LI` | `CI` | `010` | ✅ |
| `C.ADDI16SP, C.LUI` | `CI` | `011` | ✅ |
| `C.SRLI, S.SRLI64, C.SRAI64, C.ANDI, C.SUB, C.XOR, C.OR, C.AND, C.SUBW, C.ADDW` | `~` | `100` | ✅ |
| `C.J` | `CR` | `101` | ✅ |
| `C.BEQZ` | `CB` | `110` | ✅ |
| `C.BNEZ` | `CB` | `111` | ✅ |
**`C2`**
| Function | Format | Funct3 Bits | Supported |
| ------------------------------------- | ------ | ----------- | --------- |
| `C.SLLI64` | `CI` | `000` | ✅ |
| `C.FLDSP` | `CI` | `001` | ❌ |
| `C.LWSP` | `CI` | `010` | ✅ |
| `C.LDSP` | `CI` | `011` | ✅ |
| `C.JR, C.MV, C.EBREAK, C.JALR, C.ADD` | `~` | `100` | ✅ |
| `C.FSDSP` | `CSS` | `101` | ❌ |
| `C.SWSP` | `CSS` | `110` | ✅ |
| `C.SDSP` | `CSS` | `111` | ✅ |
In order to most easily integrate with the existing `asterisc` codebase and support a nearly-identical implementation between Yul and Go, I put together a `DecompressInstruction` helper in a common package within `rvgo` that can wrap the `readMem` call that fetches the instruction within the VM step. If the instruction is 32 bits, we return early with the original instruction, else we fall back to a large switch statement that does the translation.
To generate the decompression switch case, I spun up a simple python script that will compute a unique 5 bit selector for each compressed op/funct3 combination and template out the switch statement:
```python
# Instructions from the RVC extension of RISC-V, sorted by opcode -> funct3
# Tuple format: name - type - opcode - funct3
C_INSTRS = [
# C0
('C.ADDI4SPN', 'CIW', 0, 0),
('C.FLD', 'CL', 0, 1),
('C.LW', 'CL', 0, 2),
('C.LD', 'CL', 0, 3),
('Reserved', '~', 0, 4),
('C.FSD (Unsupported)', 'CS', 0, 5),
('C.SW', 'CS', 0, 6),
('C.SD', 'CS', 0, 7),
# C1
('C.NOP, C.ADDI', 'CI', 1, 0),
('C.ADDIW', 'CI', 1, 1),
('C.LI', 'CI', 1, 2),
('C.ADDI16SP, C.LUI', 'CI', 1, 3),
('C.SRLI, S.SRLI64, C.SRAI64, C.ANDI, C.SUB, C.XOR, C.OR, C.AND, C.SUBW, C.ADDW', '?', 1, 4),
('C.J', 'CR', 1, 5),
('C.BEQZ', 'CB', 1, 6),
('C.BNEZ', 'CB', 1, 7),
# C2
('C.SLLI64', 'CI', 2, 0),
('C.FLDSP (Unsupported)', 'CI', 2, 1),
('C.LWSP', 'CI', 2, 2),
('C.LDSP', 'CI', 2, 3),
('C.JR, C.MV, C.EBREAK, C.JALR, C.ADD', '?', 2, 4),
('C.FSDSP (Unsupported)', 'CSS', 2, 5),
('C.SWSP', 'CSS', 2, 6),
('C.SDSP', 'CSS', 2, 7)
]
def compute_switch_selector(op: int, funct3: int) -> int:
"""
Computes a unique 5 bit switch statement selector for an opcode / funct3 combination from a RVC instruction.
Format:
Funct3 | Op
vvvvvvvvvvvv|vvvvvvvv
┌───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │
└───┴───┴───┴───┴───┘
"""
return ((funct3 << 2) & 0x1C) | (op & 0x3)
# Precompute switch selectors for all `C_INSTRS` and sort by the selectors.
C_INSTRS = [(instr[0], instr[1], instr[2], instr[3], compute_switch_selector(instr[2], instr[3])) for instr in C_INSTRS]
C_INSTRS.sort(key=lambda x: x[4])
# ...
```
Now that I had the list of instructions I need to support translation for, I got to bit twiddling.
> This is my first foyer into RISC-V, and one thing I was caught a bit off-guard by is the immediate formats in the instructions. Why on earth is everything so jumbled around? Turns out of course there's a reason, and it's pretty neat. [*Why are RISC-V Immediates so weird?*](https://youtu.be/a7EPIelcckk) by Julian Delphiki is a great explainer on this subject, but as a TL;DR for the information laid out in that video:
> 1. Much of the time, the `immediate` in RISC-V instructions needs to be sign extended. Bit `31` of the instruction is *always* the most significant bit of the `immediate`, which allows for some significant hardware optimizations (reducing the adverse affects of [fan-out](https://en.wikipedia.org/wiki/Fan-out)). Since sign extension can actually be the bottleneck of the instruction processing, this tradeoff was worth-while for the RISC-V ISA designers.
> 1. Less multiplexers! Because the immediate fields line up between instruction types (for the most part), less muxes are needed to parse the instructions in the hardware decoder.
> 
My decompression file ended up consisting of several tedious functions for decoding different `C` extension instructions, some bit twiddling to re-order the immediates, and a few functions for re-encoding the parsed values into the 32 bit standard instructions. At a high level, these mappings look like:
<center>
<img src="https://hackmd.io/_uploads/rJX3b33I6.png"/>
</center>
### Interpreting `C` extension Instructions
Because the `C` extension enables 16-bit instructions, it is now possible for `asterisc` to read 1, 1 1/2, or 2 instructions at once when executing an instruction step. This is due to the static size of the memory read when loading instructions; 32 bits will always be loaded.
<center>
<img src="https://hackmd.io/_uploads/BJmr4ZOIp.png">
</center>
<br>
The solution for this is rather simple; Because `asterisc` always performs a static 4 byte load from memory starting at the current program counter, we just adjust the number of bytes that we increment the program counter by to the the size of the executed instruction (in bytes):
| Compressed? | PC Increment |
| ----------- | ------------ |
| ✅ | 2 |
| ❌ | 4 |
At a high-level, the alterations introduce a branch at the time of instruction loading like so:
<center>
<img src="https://hackmd.io/_uploads/HJ9JU7q86.svg" height="450px">
</center>
<br/>
There was also a small issue in the alignment when reading instructions, particularly when reading over 4 byte boundaries. `asterisc` previously expected every load to be 4-byte aligned, whereas now, it is possible to perform reads on half-words. RISC-V's spec does cover this, so all there was to do was alter the memory loading function to expect these reads depending on the alignment of the program counter:

### Testing
To test compliance of my `C` extension instruction decompressor, I integrated the `rv64uc` tests from [`riscv-software-src/riscv-tests`](https://github.com/riscv-software-src/riscv-tests/blob/master/isa/rv64uc/rvc.S) into `asterisc`'s existing `riscv-tests` module that pulls in several other test vectors from this suite.
{%pdf https://riscv.org/wp-content/uploads/2015/01/riscv-testing-frameworks-bootcamp-jan2015.pdf %}
The process for getting these to work was actually pretty smooth; The build script provides several dump files with human-readable RISC-V assembly for each of the tests. Each time one failed, you can just jump to the PC in the dump to see what went wrong. Most of the errors I ran into were off-by-one's in bitshifts, etc., and after fixing those up, it was showing that `asterisc` was now passing the `rv64uc` tests:

## Syscalls & `glibc`
LLVM & `glibc` use a variety of different syscalls from Golang, and `glibc` also performs some bootstrapping that Golang does not. On the first go of running the Rust ELF on `asterisc` after passing the `rv64uc` tests, I ran into the next issue: `FATAL: kernel too old`.
### Kernel Too Old

Turns out, `glibc` has a [check within `sysv`](https://github.com/lattera/glibc/blob/master/sysdeps/unix/sysv/linux/dl-osinfo.h#L29) that attempts to read the Kernel version from the `uname` syscall, and will emit a fatal error if the kernel version could not be read or is deemed too old. Because `asterisc` is a non-traditional Linux kernel, I opted to patch this out of the ELF while loading it into the VM.
`asterisc` already has several symbols that it patches out for various reasons, namely Golang's garbage collection. Since `asterisc` has no support for threads, and Go's GC spins out goroutines in order to garbage collect, Go programs running on top of asterisc cannot GC. The code to patch these out inserts a pseudo instruction, `jalr zero, ra, 0`, which jumps directly to the return address in register `$ra`. Since I don't care about the kernel check for Asterisc programs, I added the `__dl_discover_osversion` symbol to the list of patched symbols and got on my way.
Other symbols patched out are a bit more self-explanatory, and the list ended up looking like:
```
"__geteuid", // glibc user / group checks aren't needed
"__getuid", // glibc user / group checks aren't needed
"__getegid", // glibc user / group checks aren't needed
"__getgid", // glibc user / group checks aren't needed
"_dl_discover_osversion", // disable glibc kernel version check
"__pthread_initialize_minimal_internal", // patch out pthreads, asterisc is sync atm
"_dl_get_origin": // No dynamic loader / shared libs
```
The invocation of the dynamic loader is an issue. Programs on asterisc must be 100% statically linked at the moment, which the `riscv64gc-unknown-linux-gnu` target's distributed toolchain isn't providing. For the sake of getting "hello world" running, though, we can patch it out for now, but we'll need dynamic linking capabilities during ELF loading in `asterisc` to support a wider variety of Rust programs on this target.
### Unsupported Syscalls
Currently, `asterisc` only supports syscalls used by Golang, documented [in the repository](https://github.com/protolambda/asterisc/blob/master/docs/golang.md#linux-syscalls-used-by-go). To get the minimal "hello, world!" example running, I added support for the following [RISC-V linux syscalls](https://jborza.com/post/2021-05-11-riscv-linux-syscalls/):
- [`faccessat`](https://man7.org/linux/man-pages/man2/faccessat.2.html) - hardcode to 0, fd access is always granted in asterisc.
- [`ppoll_time32`](https://aquasecurity.github.io/tracee/v0.12/docs/events/builtin/syscalls/ppoll_time32/) - yield nothing, synchronous execution only.
- [`set_tid_address`](https://man7.org/linux/man-pages/man2/set_tid_address.2.html) - hardcode to 0 for determinism.
After that, our program runs on top of native `asterisc`!

## Bare Metal Target
Because the `riscv64gc-unknown-linux-gnu` target attempts to link `glibc` and expects a more feature-complete Linux kernel, I also spun up some utilities for writing programs on top of the `no_std` bare metal target, `riscv64gc-unknown-none-elf`. Because the target doesn't include the standard library, I had to write a few utilities for performing a select number of syscalls, and also to set up a global allocator (the `alloc` crate can be used with `asterisc`). This target still prefers `c` extension instructions, so our above work is still helpful here!
### Syscalls
Syscalls in RISC-V are performed through the `ECALL` instruction, with the following registers being used for inputs & return values:
| Register | Description |
| -------- | ------------------- |
| `$a0` | arg1 + return value |
| `$a1` | arg2 |
| `$a2` | arg3 |
| `$a3` | arg4 |
| `$a4` | arg5 |
| `$a5` | arg6 |
| `$a7` | syscall number |
For example, for the [`write`](https://man7.org/linux/man-pages/man2/write.2.html) syscall, we have to perform an `ECALL` with 3 arguments:
```rs
#[inline]
pub unsafe fn write(fd: u64, buf_ptr: u64, buf_len: u64) -> u64 {
// `write` syscall number: https://jborza.com/post/2021-05-11-riscv-linux-syscalls/
const WRITE_SYSNO: usize = 64;
let mut ret: u64;
asm!(
"ecall",
in("a7") WRITE_SYSNO,
inlateout("a0") fd => ret,
in("a1") buf_ptr,
in("a2") buf_len,
options(nostack, preserves_flags)
);
ret
}
```
### Running `revm`
Featuring a soon^tm library for developing verifiable Rust programs on top of the OP Stack's various fault proof VMs, we're even able to run an EVM state transition through `revm`! This is the first time an EVM alternative to `geth`'s has been fault proven on one of the [OP Stack][op-stack]'s fault proof VMs, albeit with static inputs, which is pretty exciting.
```rs
#![no_std]
#![no_main]
use alloc::string::String;
use kona_common::{alloc_heap, asterisc::io::FileDescriptor, io::ClientIO, traits::BasicKernelIO};
use revm::{
db::{CacheDB, EmptyDB},
primitives::{address, bytes, hex, Address, ExecutionResult, Output, TransactTo, U256},
EVM,
};
extern crate alloc;
/// Static size of heap; 50MB should be enough for this test.
const HEAP_SIZE: usize = 50_000_000;
const SENDER: Address = address!("000000000000000000000000000000000badc0de");
const ID_PRECOMPILE: Address = address!("0000000000000000000000000000000000000004");
#[no_mangle]
pub extern "C" fn _start() {
alloc_heap!(HEAP_SIZE);
// Set up the EVM
let cachedb = CacheDB::new(EmptyDB::new());
let mut evm = EVM::new();
evm.database(cachedb);
// Fill TX env - call from address(0xbadc0de) -> address(4) w/ payload "Hello, asterisc!"
evm.env.tx.caller = SENDER;
evm.env.tx.transact_to = TransactTo::Call(ID_PRECOMPILE);
evm.env.tx.data = bytes!("48656c6c6f2c20617374657269736321");
evm.env.tx.value = U256::from(0);
// execute transaction without writing to the DB
let ref_tx = evm.transact_ref().expect("stateless transact failed");
// Unpack returndata
let (output, reason) = match ref_tx.result {
ExecutionResult::Success {
output: Output::Call(output),
reason,
..
} => (output, reason),
result => panic!("Execution failed: {result:?}"),
};
// Assert that the ID precompile correctly returned our bytes
assert_eq!(output, evm.env.tx.data);
// Print result to stdout
let _ = ClientIO::write(
FileDescriptor::StdOut,
alloc::format!(
"Call result: 0x{} (\"{}\") | Reason: {:?}\n",
hex::encode(output.clone()),
String::from_utf8(output.to_vec()).expect("failed to parse utf-8"),
reason,
)
.as_bytes(),
);
// Exit with success
ClientIO::exit(0);
}
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
let msg = alloc::format!("Panic: {}", info);
let _ = ClientIO::write(FileDescriptor::StdErr, msg.as_bytes());
ClientIO::exit(2)
}
```

## Next Steps
`asterisc` is a very promising compliment to `cannon` / `cannon-rs`, and I'd like to see it brought to production readiness over the coming months. The presence of a secondary fault proof VM is also on the critical path towards the [OP Stack][op-stack]'s stage 2 decentralization.
Looking past productionizing `asterisc` with its current feature set, a few things are on my wish list:
* More comprehensive support for Rust programs. There's a good bit more to go than the work done in this post to cover syscalls used by the compilation backend, creating a smooth build pipeline, etc.
* The `RVC` extension decompressor will also need to be ported to the `slow` and `solc` version of `asterisc`.
* A Rust library for developing `client` and `host` programs on the various FPVMs, including `asterisc`, with abstractions that allow cross-compilation.
* Support for deterministic context switching between threads. This will allow for a much wider range of programs to be ran!
* Update the memory code to use a radix tree rather than a map for constant-time pointer access. Currently the memory implementation is also shared for the most part with `cannon`, so this is a double-win in terms of implementation diversity & performance (h/t [proto][protolambda]).
* Alternative implementation in Rust or Zig. Rust or Zig's type system / metaprogramming paradigms would allow for a significant reduction in code, specifically around the arithmetic / logical integer operation abstractions. At the cost of complexity, these languages also allow us to tap into certain optimizations that are harder to perform in Golang, such as cache line alignment for the memory structures.
* *Possibly*: Support for dynamic linking of shared libraries upon ELF loading. Complexity bad, though.
And that's it, you made it to the bottom! Wasn't so bad, eh? If you're an experienced OSS Rust developer, this sort of work excites you, and you're interested in joining a team of Rust engineers @ OP Labs on a warpath to stage 2 decentralization, please reach out on Twitter [@vex_0x](https://twitter.com/vex_0x)!
[^1]: https://www2.eecs.berkeley.edu/Pubs/TechRpts/2015/EECS-2015-209.pdf
[^2]: https://fprox.substack.com/p/riscv-c-extension
[op-stack]: https://github.com/ethereum-optimism/optimism
[cannon]: https://github.com/ethereum-optimism/optimism/tree/develop/cannon
[cannon-rs]: https://github.com/anton-rs/cannon-rs
[protolambda]: https://github.com/protolambda
[asterisc]: https://github.com/protolambda/asterisc
[rust-platform-support]: https://doc.rust-lang.org/rustc/platform-support.html#tier-2-with-host-tools
[cross]: https://github.com/cross-rs/cross