Expand the runtime
By default, Roto’s standard library is minimal. The idea behind that is that each application should supply the functionality that they allow the scripts to use.
The standard runtime doesn’t even have a way to print to stdout, because
the host application may not want to allow the scripts to do that. But it is
possible to add some standard modules to the runtime. For example, there is a
method add_io_functions on Runtime to add the print function.
use roto::Runtime;
let mut rt = Runtime::new();
rt.add_io_functions();
However, Roto truly starts shining when you add your own functionality, so that your application becomes scriptable.
We support this via Runtime::from_lib and Runtime::add in combination
with the library! macro. You construct a library with that macro and then
add it to your runtime by calling one of those methods. Runtime::from_lib
is simply a convenience method for creating a new runtime and adding the library
directly.
Add functions
Even though Roto provides a standard print function, you might not want to
expose that. For instance, you might want to let scripts print to a file
instead. We can do that quite easily with the library! macro. Any function
declared within that macro will then become available in the scripts you
compile.
Note
We use Arc<str> because that’s the Rust type corresponding to Roto’s
String type. You always have to use the Rust names of types when
registering functions. To learn how the types correspond to each other, see
Rust Interoperability.
use std::sync::Arc;
use std::fs::OpenOptions;
use std::io::Write;
use roto::{Runtime, roto_function};
let lib = library!{
/// Print to the log file.
fn print(s: Arc<str>) {
let mut file = OpenOptions::new()
.create(true)
.write(true)
.append(true)
.open("log.txt")
.unwrap();
let _ = writeln!(file, "{s}");
}
};
let rt = Runtime::from_lib(lib).unwrap();
Scripts compiled with this runtime can then use this custom print function.
The registered functions can have any name you want, as long as it’s a valid
Roto identifier.
Here is another example which returns a value:
let lib = library! {
fn factorial(n: u32) -> u32 {
(1..=n).fold(1, |a, i| a * i)
}
};
Add types
We can also add our own types to Roto. As an example, we’ll add a Range
type. All registered types must implement Clone. Every type you register
must be wrapped in Val<T>, which tells Roto that it is a custom type and
not built in.
use roto::{library, Runtime, Val};
#[derive(Clone, Copy, Debug)]
struct Range {
low: i64,
high: i64,
}
let lib = library! {
/// A range of i64 numbers
#[copy] type Range = Val<Range>;
};
let rt = Runtime::from_lib(lib).unwrap();
The argument to that method is the docstring for this type. We can now pass this type to Roto and return it from Roto:
fn passthrough(x: Range) -> Range {
x
}
Not very useful yet, of course, but let’s see it in action anyway:
use roto::Val;
let mut pkg = rt.compile("script.roto").unwrap();
let f = pkg
.get_function::<fn(Val<Range>) -> Val<Range>>("passthrough")
.unwrap();
let res = f.call(Val(Range { low: 0, high: 99 }));
println!("{res:?}")
Note that every custom type has to be wrapped in Val when it’s passed to
Roto, but otherwise it works exactly like before.
The #[copy] attribute above specifies that the Rust type implements
Copy. If the type does not implement Copy, you can instead annotate the
declaration with #[clone]. However, you should prefer #[copy] to allow
Roto to generate code that performs slightly better.
Add methods
To make the Range type we registered previously actually useful, we can
expose methods on it to Roto.
let lib = library! {
impl Val<Range> {
fn contains(self, x: i64) -> bool {
range.low <= x && x < range.high
}
}
};
let rt = Runtime::from_lib(lib);
let mut pkg = rt.compile("script.roto").unwrap();
let f = pkg
.get_function::<_, fn(Val<Range>, x: i64) -> bool>("in_range")
.unwrap();
let range = Range { low: 0, high: 99 };
let res = f.call(Val(range), 50);
println!("{res:?}");
And then in Roto:
fn in_range(r: Range, x: i64) -> bool {
r.contains(x)
}
The first argument of a function in an impl block does not need to be of
the same type as the one specified by the impl block. If that is the case,
this function can only be called with the full path and not as a method. In the
example below new is such a method.
let lib = library! {
impl Val<Range> {
fn new(low: i64, high: i64) -> Self {
Val(Range { low, high })
}
fn contains(range: Val<Range>, x: i64) -> bool {
range.low <= x && x < range.high
}
}
};
let rt = Runtime::from_lib(lib).unwrap();
The registered new function can be used in Roto like this:
let range = Range.new(0, 99);
range.contains(50)
Add constants
Finally, we can register constants into the runtime. Like functions, we can only add constants of types we’ve already registered. Along with the constant we have to provide a docstring. This docstring will show up in the documentation generated for this runtime.
let lib = library! {
/// A range from 0 to 100
const ONE_HUNDRED: Val<Range> = Val(Range { low: 0, high: 100 });
};
let rt = Runtime::from_lib(lib).unwrap();
The name ONE_HUNDRED will then be available in Roto scripts.
Add context
In the previous section, we added constants to the Runtime, but sometimes
constants are too restrictive. One such case is when we have a value that we
want to keep constant throughout a single invocation of a function. Or, to
phrase it another way, we might want to pass in some implicit arguments that
the script has access to.
Adding context is a bit more difficult, because we need a single way to pass
all those implicit arguments. So, instead of registering each context variable
one by one, you have to create a context type by deriving the Context
trait. You can then register that type as the context you want to use.
Note
You can only register one context type per runtime.
Imagine that we save some script that operates on the data of some user. We might then expose the name of that user to all scripts implicitly. We would then create and register the following type.
use std::sync::Arc;
use roto::Context;
#[derive(Context)]
struct Ctx {
pub first_name: Arc<str>,
pub last_name: Arc<str>,
}
let rt = rt.with_context_type::<Ctx>().unwrap();
let mut pkg = rt.compile("script.roto").unwrap();
let f = pkg.get_function::<fn() -> Arc<str>>("greeting").unwrap();
let mut ctx = Ctx {
first_name: "John".into(),
last_name: "Doe".into(),
};
let greeting = f.call(&mut ctx);
println!("{greeting}");
All the fields of Ctx have to be public, to acknowledge the fact that
they are exposed to Roto. The first argument of f.call is the context we
give to this invocation. The script can then use the names of the fields of
Ctx as if they were constants.
fn greeting() -> String {
"Hello, " + first_name + " " + last_name + "!"
}
Other use-cases of context are log files, unique ids per invocation or just to provide easy access to some common data.
Add modules
Everything registered so far has been added to the global module. With a large
runtime, that quickly becomes unwieldy. We can fix this by using Roto’s module
system. To do so, simply wrap the items you want to register in a Rust-like
mod.
You can nest mod statements to create complex structures.
let lib = library! {
mod math {
/// Compute the sine of `x`
fn sin(x: f64) -> f64 {
x.sin()
}
/// Compute the cosine of `x`
fn cos(x: f64) -> f64 {
x.cos()
}
}
};
let rt = Runtime::from_lib(lib).unwrap();
The Rust code above registers the sin and cos functions in the math
module, so that’s how we can then access it from Roto:
# import the cos function so we can use it directly
import math.cos;
fn foo(x: f64) -> f64 {
# we haven't imported sin so we have to access it via `math`
math.sin(x)
}
fn foo(x: f64) -> f64 {
cos(x)
}
Composing libraries
We are not limited to using just a single library! call, but we can create
multiple libraries and compose them using the include! macro inside a
library!. Doing so includes a library directly in another at the point where
the include! macro is used.
Warning
Take care not to include items twice at different points in the hierarchy to avoid confusion.
let foo = library! {
#[clone] type Foo = Val<SomeRustType>;
};
let bar = library! {
fn bar() -> u32 {
5
}
};
let baz = library! {
// include the contents of foo directly
include!(foo)
// include the contents of bar inside the functions module
mod functions {
include!(bar)
}
};
See also
For more information, see the documentation for the library! macro.