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. .. code-block:: rust 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`` 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 :ref:`rust_interop`. .. code-block:: rust 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) { 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: .. code-block:: rust 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`, which tells Roto that it is a custom type and not built in. .. code-block:: rust 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; }; 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: .. code-block:: roto :class: test-ignore fn passthrough(x: Range) -> Range { x } Not very useful yet, of course, but let's see it in action anyway: .. code-block:: rust use roto::Val; let mut pkg = rt.compile("script.roto").unwrap(); let f = pkg .get_function::) -> Val>("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. .. code-block:: rust let lib = library! { impl Val { 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, 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: .. code-block:: roto :class: test-ignore 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. .. code-block:: rust let lib = library! { impl Val { fn new(low: i64, high: i64) -> Self { Val(Range { low, high }) } fn contains(range: Val, 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: .. code-block:: roto :class: test-ignore 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. .. code-block:: rust let lib = library! { /// A range from 0 to 100 const ONE_HUNDRED: Val = 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: 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. .. code-block:: rust use std::sync::Arc; use roto::{Context, RotoString}; #[derive(Context)] struct Ctx { pub first_name: RotoString, pub last_name: RotoString, } let rt = rt.with_context_type::().unwrap(); let mut pkg = rt.compile("script.roto").unwrap(); let f = pkg.get_function:: RotoString>("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. .. code-block:: roto :class: test-ignore 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. .. code-block:: rust 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: .. code-block:: roto :class: test-ignore # 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. .. code-block:: rust let foo = library! { #[clone] type Foo = Val; }; 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.