Rust interop
An Oxie project is a Cargo crate and Oxie code is Rust code, so interop is not a bridge or an FFI layer — it is the same compiler, the same types and the same crates.
How a project is put together
Section titled “How a project is put together”myapp/ Cargo.toml normal manifest; oxie-build is a build-dependency build.rs oxie_build::compile() — compiles src/**/*.oxi to Rust in OUT_DIR src/main.rs include!(concat!(env!("OUT_DIR"), "/oxie/main.rs")); src/main.oxi your program src/util.oxi module `util`cargo build, cargo run, cargo test, cargo doc, cargo clippy and rust-analyzer all work
on it. oxie run/oxie build call cargo and translate error locations to .oxi lines.
Using crates
Section titled “Using crates”Add dependencies to Cargo.toml as usual (cargo add serde --features derive works) and use
them:
// dep: serde = { version = "1", features = ["derive"] }// dep: serde_json = "1"import serde::{Deserialize, Serialize}
@derive(Serialize, Deserialize)data class Config(name: String, port: u16, debug: bool = false)
fun main() throws { val c: Config = serde_json.from_str("""{"name": "api", "port": 8080, "debug": true}""")? println("${c.name} on ${c.port}") println(serde_json.to_string(&c.copy(port = 9090))?)}use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]pub(crate) struct Config { pub(crate) name: String, pub(crate) port: u16, pub(crate) debug: bool,}
impl Config { #[allow(dead_code)] pub(crate) fn new(name: String, port: u16, debug: bool) -> Self { Self { name, port, debug, } }}
fn __oxie_main() -> ::std::result::Result<(), crate::__oxie::AnyError> { let c: Config = serde_json::from_str("{\"name\": \"api\", \"port\": 8080, \"debug\": true}")?; println!("{} on {}", c.name, c.port); println!("{}", serde_json::to_string(&({ let mut __oxie_copy1 = c.clone(); __oxie_copy1.port = 9090; __oxie_copy1 }))?); Ok(())}
fn main() { if let Err(err) = __oxie_main() { crate::__oxie::report(err); ::std::process::exit(1); }}api on 8080{"name":"api","port":9090,"debug":true}(The // dep: lines are only needed for single-file scripts; projects use Cargo.toml.)
- Attributes are written
@name(...); derive macros merge with Oxie’s automatic derives. - Crate paths can use
.or:::serde_json.from_str,tokio::spawn,rand.random(). - Macros from crates are called with
!as in Rust:json!({ "a": 1 }),tokio::join!(a, b).
Calling Rust from Oxie
Section titled “Calling Rust from Oxie”Hand-written Rust modules can live in the same crate. In an Oxie project, declare them from a
.oxi file:
// not-runnable: needs src/legacy.rs next to this filemod legacy // src/legacy.rs
fun main() = println(legacy::answer())#[path = "/tmp/oxie-docs-lAwK6a/legacy/mod.rs"]mod legacy;
fn main() { println!("{}", legacy::answer());}Any Rust function, type, trait or macro is then used directly. Rust-only syntax can be written
inline with rust { ... }.
Calling Oxie from Rust
Section titled “Calling Oxie from Rust”Everything Oxie declares is a normal Rust item. In a Rust crate, add Oxie modules with
oxie init (inside the crate), then include the generated modules once:
// src/main.rs or src/lib.rsinclude!(concat!(env!("OUT_DIR"), "/oxie/mod.rs")); // declares every src/*.oxi module
fn main() { let invoice = pricing::Invoice::new("ACME".to_string(), vec![]); println!("{}", invoice);}See examples/mixed: a Rust binary whose pricing module is written in
Oxie and itself calls back into Rust.
What the generated Rust looks like:
| Oxie | Rust you call |
|---|---|
data class P(x: f64) |
struct P { pub(crate) x: f64 }, P::new(x), derives |
class C(a: A) { fun m() } |
struct C, C::new(a), c.m() |
enum E { V(n: i32) } |
enum E { V { n: i32 } } |
interface I { fun f(): i32 } |
trait I { fn f(&self) -> i32; } |
fun f(): T throws |
fn f() -> Result<T, Box<dyn Error + Send + Sync>> |
fun String.ext() |
trait __OxieExt_<module>_ext implemented for String (import it to call) |
object O { fun f() } |
mod O { pub(crate) fn f() } |
Writing a library in Oxie
Section titled “Writing a library in Oxie”Use oxie new mylib --lib (root src/lib.oxi). Mark the public API pub; members of a pub
class are public automatically:
// not-runnable: library codepub data class Point(x: f64, y: f64) { fun distance(o: Point): f64 = ((x - o.x).powi(2) + (y - o.y).powi(2)).sqrt()}
pub fun origin() = Point(0.0, 0.0)#[derive(Debug, Clone, Copy, PartialEq)]pub struct Point { pub x: f64, pub y: f64,}
impl Point { #[allow(dead_code)] pub fn new(x: f64, y: f64) -> Self { Self { x, y, } }
pub fn distance(&self, o: Point) -> f64 { ((self.x - o.x).powi(2) + (self.y - o.y).powi(2)).sqrt() }}
pub fn origin() -> Point { Point::new(0.0, 0.0)}Rust users depend on it like any crate: geometry::Point::new(1.0, 2.0).distance(...).
examples/geometry has an integration test written in Rust.
Generated code
Section titled “Generated code”- Generated files are in
target/<profile>/build/<crate>-*/out/oxie/(one.rsper.oxi, plus__oxie.rs, the tiny support module, and.mapsource maps). oxie emit file.oxiprints them;oxie emit --annotate file.oxiadds a// file.oxi:linecomment above each statement, to find the Rust for a line of your code.- The support module contains only zero-cost helpers: the
AnyErrortype alias, the?:helpers (#[inline(always)]trait methods onOption/Result),.context(), and the error printer formain() throws. There is no runtime.
Using Rust tools
Section titled “Using Rust tools”- rust-analyzer works on the generated code (useful for exploring types); the Oxie language
server (
oxie lsp) works on.oxifiles. - Debuggers (lldb, gdb) step through the generated Rust, which mirrors the
.oxisource closely. - Panics point at
.oxilines underoxie runandoxie test; run withcargo runor a debugger, they report the generated file (the.mapnext to it gives the.oxiline). - clippy / rustfmt run on the crate; generated code is formatted for reading but isn’t meant to be edited.