Introduction
Note: Mun & this book are currently under active development, any and all content of this book is not final and may still change.
Mun is an embeddable scripting language designed for developer productivity.
-
Ahead of time compilation
Mun is compiled ahead of time (AOT), as opposed to being interpreted or compiled just in time (JIT). By detecting errors in the code during AOT compilation, an entire class of runtime errors is eliminated. This allows developers to stay within the comfort of their IDE instead of having to switch between the IDE and target application to debug runtime errors. -
Statically typed
Mun resolves types at compilation time instead of at runtime, resulting in immediate feedback when writing code and opening the door for powerful refactoring tools. -
First class hot-reloading
Every aspect of Mun is designed with hot reloading in mind. Hot reloading is the process of changing code and resources of a live application, removing the need to start, stop and recompile an application whenever a function or value is changed. -
Performance
AOT compilation combined with static typing ensure that Mun is compiled to machine code that can be natively executed on any target platform. LLVM is used for compilation and optimization, guaranteeing the best possible performance. Hot reloading does introduce a slight runtime overhead, but it can be disabled for production builds to ensure the best possible runtime performance. -
Cross compilation
The Mun compiler is able to compile to all supported target platforms from any supported compiler platform. -
Powerful IDE integration
The Mun language and compiler framework are designed to support source code queries, allowing for powerful IDE integrations such as code completion and refactoring tools.
Case Studies
A collection of case studies that inspired the design choices made in Mun.
Abbey Games
Abbey Games uses Lua as its main gameplay programming language because of Lua's ability to hot reload code. This allows for rapid iteration of game code, enabling gameplay programmers and designers to quickly test and tweak systems and content. Lua is a dynamically typed, JIT compiled language. Although this has some definite advantages, it also introduces a lot of problems with bigger codebases.
Changes in Lua code can have large implications throughout the entire codebase and since we cannot oversee the entire codebase at all times runtime errors are bound to occur. Runtime errors are nasty beasts because they can pop up after a long period of time and after work on the offending piece of code has already finished. They are also often detected by someone different from the person who worked on the code. This causes great frustration and delay, let alone when the runtime error is detected by a user of the software.
Lua amplifies this issue due to its dynamic and flexible nature. It would be great if we could turn some of these runtime errors into compile time errors. That way programmers are notified of errors way before someone else runs into them. The risk of causing implicit runtime errors causes programmers to distrust their refactoring tools. This in turn reduces the likelihood of programmers refactoring their code.
Even though Lua offers immense flexibility, we noticed that certain opinionated patterns recur a lot and as such have become standard practice. Introducing these practices assists us in daily development a lot, but requires more code and complexity than desirable. Having syntactic sugar would greatly help reduce complexity in our code base, but would also introduce magic or custom keywords that are foreign to both new developers and IDE's.
Rapid iteration is key to prototyping game concepts and features. Proper IDE-integration of a scripting language gives a huge boost to productivity.
Getting Started
Let's start your Mun journey by installing the Mun CLI and creating a simple Mun library. We'll then show you how to make it hot reloadable by embedding it into an application.
Installation
First we need to install the Mun CLI (command-line interface), which acts as an all-in-one tool for Mun application development. Pre-built binaries are available for macOS, Linux, and Windows (64-bit only). Download and extract the binaries to a location of your preference.
You are now ready to write your first Mun code!
Hello, fibonacci?
Most programming languages start off with a "Hello, world!" example, but not Mun. Mun is designed around the concept of hot reloading. Our philosophy is to only add new language constructs when those can be hot reloaded. Since the first building blocks of Mun were native types and functions our divergent example has become fibonacci, hence "Hello, fibonacci?".
Creating a Project Directory
The Mun compiler is agnostic to the location of a project directory, as long as all source files are in the same place. Let's open a terminal to create our first project directory:
mkdir hello_fibonacci
cd hello_fibonacci
Writing and Running a Mun Library
Next, make a new source file and call it hello_fibonacci.mun. Mun files always end with the .mun extension. If your file name consists of multiple words, separate them using underscores.
Open up the new source file and enter the code in Listing 1-1.
Filename: hello_fibonacci.mun
pub fn fibonacci_n() -> i64 {
let n = arg();
fibonacci(n)
}
fn arg() -> i64 {
5
}
fn fibonacci(n: i64) -> i64 {
if n <= 1 {
n
} else {
fibonacci(n - 1) + fibonacci(n - 2)
}
}
Save the file and go back to your terminal window. You are now ready to compile your first Mun library. Enter the following command to compile the file:
mun build hello_fibonacci.mun
Contrary to many other languages, Mun doesn't support standalone applications,
instead it is shipped in the form of Mun libraries - recognisable by their
*.munlib
extension. That's why Mun comes with a command-line interface (CLI)
that can both compile and run Mun libraries. To run a Mun library, enter the
following command:
mun start hello_fibonacci.munlib --entry fibonacci_n
The result of fibonacci_n
(i.e. 5
) should now appear in your terminal.
Congratulations! You just successfully created and ran your first Mun library.
Hello, Hot Reloading!
Mun distinguishes itself from other languages by its inherent hot reloading
capabilities. The following example illustrates how you can create a hot
reloadable application by slightly modifying the Hello,
fibonacci? example. In Listing 1-2, the
fibonacci_n
function has been removed and the pub
keyword has been added to
both args
and fibonacci
.
Filename: hello_fibonacci.mun
pub fn arg() -> i64 {
5
}
pub fn fibonacci(n: i64) -> i64 {
if n <= 1 {
n
} else {
fibonacci(n - 1) + fibonacci(n - 2)
}
}
Apart from running Mun libraries from the command-line interface, a common use case is embedding them in other programming languages.
Mun embedded in C++
Mun exposes a C API and complementary
C++ bindings for the Mun Runtime. Listing 1-3 shows a C++ application that
constructs a Mun Runtime for the hello_fibonacci
library and continuously
invokes the fibonacci
function and outputs its result.
Filename: main.cc
#include <iostream>
#include "mun/runtime.h"
int main() {
if (argc < 2) {
return 1;
}
auto lib_path = argv[1];
if (auto runtime = mun::make_runtime(lib_path)) {
while (true) {
auto arg = mun::invoke_fn<int64_t>(*runtime, "arg").wait();
auto result =
mun::invoke_fn<int64_t>(*runtime, "fibonacci", arg).wait();
std::cout << "fibonacci(" << std::to_string(arg) << ") = " << result
<< std::endl;
runtime->update();
}
}
return 2;
}
Mun embedded in Rust
As the Mun Runtime is written in Rust, it can be easily embedded in Rust
applications by adding the mun_runtime
crate as a dependency. Listing 1-4
illustrates a simple Rust application that builds a Mun Runtime and continuously
invokes the fibonacci
function and prints its output.
Filename: main.rs
extern crate mun_runtime;
use mun_runtime::{invoke_fn, RetryResultExt, RuntimeBuilder};
use std::{cell::RefCell, env, rc::Rc};
fn main() {
let lib_path = env::args().nth(1).expect("Expected path to a Mun library.");
let mut runtime = RuntimeBuilder::new(lib_path)
.spawn()
.expect("Failed to spawn Runtime");
loop {
let arg: i64 = invoke_fn!(runtime, "arg").wait();
let result: i64 = invoke_fn!(runtime, "fibonacci", arg).wait();
println!("fibonacci({}) = {}", arg, result);
runtime.borrow_mut().update();
}
}
Hot Reloading
The prior examples both update the runtime every loop cycle. In the background, this detects recompiled code and reloads the resulting Mun libraries.
To ensure that the Mun compiler recompiles our code every time the
hello_fibonacci.mun source file from Listing 1-2 changes, the --watch
argument must be added:
mun build hello_fibonacci.mun --watch
When saved, changes in the source file will automatically take effect in the
running example application. E.g. change the return value of the arg
function
and the application will log the corresponding Fibonacci number.
Some changes, such as a type mismatch between the compiled application and the hot reloadable library, can lead to runtime errors. When these occur, the runtime will log the error and halt until an update to the source code arrives.
That's it! Now you are ready to start developing hot reloadable Mun libraries.
Basic Concepts
This section describes the basic concepts of the Mun programming language.
Values and types
Mun is a statically typed language, which helps to detect type-related errors at compile-time. A type error is an invalid operation on a given type, such as an integer divided by a string, trying to access a field that doesn't exist, or calling a function with the wrong number of arguments.
Some languages require a programmer to explicitly annotate syntactic constructs with type information:
int foo = 3 + 4;
However, often variable types can be inferred by their usage. Mun uses type inferencing to determine variable types at compile time. However, you are still forced to explicitly annotate variables in a few locations to ensure a contract between interdependent code.
fn bar(a: i32) -> i32 {
let foo = 3 + a;
foo
}
Here, the parameter a
and the return type must be annotated because it
solidifies the signature of the function. The type of foo
can be inferred
through its usage.
NOTE: Although the above works, as of version 0.2, Mun is not yet very good at type inferencing. This will be improved in the future.
Integer types
An integer is a number without a fractional component. Table 3-1 shows the built-in integer types in Mun. Each variant can be either signed or unsigned and has an explicit size. Signed and unsigned refer to whether it is necessary to have a sign that indicates the possibility for the number to be negative or positive.
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
Signed integer types start with i
, unsigned integer types with u
, followed
by the number of bits that the integer value takes up. Each signed variant can
store numbers from -(2n - 1) to 2n - 1 - 1 inclusive,
where n is the number of bits that variant uses. Unsigned variants can store
numbers from 0 to 2n - 1. By default Mun uses 32-bit signed
integers.
The size of the isize
and usize
types depend on the target architecture. On
64-bit architectures,isize
and usize
types are 64 bits large, whereas on 32-bit
architectures they are 32 bits in size.
Floating-Point Types
Real (or floating-point) numbers (i.e. numbers with a fractional component)
are represented according to the IEEE-754 standard. The f32
type is a
single-precision float of 32 bits, and the f64
type has double precision -
requiring 64 bits.
fn main() {
let f = 3.0; // f64
}
The Boolean Type
The bool
(or boolean) type has two values, true
and false
, that are
used to evaluate conditions. It takes up one 1 byte (or 8 bits).
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
Literals
There are three types of literals in Mun: integer, floating-point and boolean literals.
A boolean literal is either true
or false
.
An integer literal is a number without a decimal separator (.
). It can be
written as a decimal, hexadecimal, octal or binary value. These are all
examples of valid literals:
let a = 367;
let b = 0xbeaf;
let c = 0o76532;
let d = 0b0101011;
A floating-point literal comes in two forms:
- A decimal number followed by a dot which is optionally followed by another decimal literal and an optional exponent.
- A decimal number followed by an exponent.
Examples of valid floating-point literals are:
let a: f64 = 3.1415;
let b: f64 = 3.;
let c: f64 = 314.1592654e-2;
Separators
Both integer and floating-point literals can contain underscores (_
) to
visually separate numbers from one another. They do not have any semantic
significance but can be useful to the eye.
let a: i64 = 1_000_000;
let b: f64 = 1_000.12;
Type suffix
Integer and floating-point literals may be followed by a type suffix to explicitly specify the type of the literal.
Literal type | Suffixes |
---|---|
Integer | u8 , i8 , u16 , i16 , u32 , i32 , u64 , i64 , i128 , u128 , usize , isize , f32 , f64 |
Floating-point | f32 , f64 |
Note that integer literals can have floating-point suffixes. This is not the case the other way around.
let a: u8 = 128_u8;
let b: i128 = 99999999999999999_i128;
let c: f32 = 10_f32; // integer literal with float suffix
When providing a literal, the compiler will always check if a literal value will fit the type. If not, an error will be emitted:
let a: u8 = 1123123124124_u8; // literal out of range for `u8`
Numeric operations
Mun supports all basic mathematical operations for number types: addition, subtraction, division, multiplication, and remainder.
fn main() {
// addition
let a = 10 + 5;
// subtraction
let b = 10 - 4;
// multiplication
let c = 5 * 10;
// division
let d = 25 / 5;
// remainder
let e = 21 % 5;
}
Each expression in these statements uses a mathematical operator and evaluates to a single value. This is valid as long as both sides of the operator have the same type.
Unary operators are also supported:
fn main() {
let a = 4;
// negate
let b = -a;
let c = true;
// not
let d = !c;
}
Shadowing
Redeclaring a variable by the same name with a let
statement is valid and will
shadow any previous declaration in the same block. This is often useful if you
want to change the type of a variable.
let a: i32 = 3;
let a: f64 = 5.0;
Use before initialization
All variables in Mun must be initialized before usage. Uninitialized variables can be declared but they must be assigned a value before they can be read.
let a: i32;
if some_conditional {
a = 4;
}
let b = a; // invalid: a is potentially uninitialized
Note that declaring a variable without a value is often a bad code smell since
the above could have better been written by returning a value from the
if
/else
block instead of assigning to a
. This avoids the use of an
uninitialized value.
let a: i32 = if some_conditional {
4
} else {
5
}
let b = a;
Functions
Together with struct
, functions are the core building blocks of hot reloading
in Mun. Throughout the documentation you've already seen a lot of examples of
the fn
keyword, which is used to define a function.
Mun uses snake case as the conventional style for function and variable names. In snake case all letters are lowercase and words are separated by underscores.
fn main() {
another_function();
}
fn another_function() {
}
Function definitions start with an optional access modifier (pub
), followed
by the fn
keyword, a name, an argument list enclosed by parentheses, an
optional return type specifier, and finally a body.
Marking a function with the pub
keyword allows you to publicly expose
that function, for usage in other modules or when hot reloading. Otherwise
the function will only be accessible from the current source file.
Function Access Modifier
Marking a function with the pub
keyword allows you to use it from outside of
the module it is defined in.
// This function is not accessible outside of this code
fn foo() {
// ...
}
// This function is accessible from anywhere.
pub fn bar() {
// Because `bar` and `foo` are in the same file, this call is valid.
foo()
}
When you want to interface from your host language (C++, Rust, etc.) with Mun,
you can only access pub
functions. These functions are hot reloaded by the
runtime when they or functions they call have been modified.
Function Arguments
Functions can have an argument list. Arguments are special variables that are part of the function signature. Unlike regular variables you have to explicitly specify the type of the arguments. This is a deliberate decision, as type annotations in function definitions usually mean that the compiler can derive types almost everywhere in your code. It also ensures that you as a developer define a contract of what your function can accept as its input.
The following is a rewritten version of another_function
that shows what an
argument looks like:
fn main() {
another_function(3);
}
fn another_function(x: i32) {
}
The declaration of another_function
specifies an argument x
of the i32
type. When you want a function to use multiple arguments, separate them with
commas:
fn main() {
another_function(3, 4);
}
fn another_function(x: i32, y: i32) {
}
Function Bodies
Function bodies are made up of a sequence of statements and expressions. Statements are instructions that perform some action and do not return any value. Expressions evaluate to a result value.
Creating a variable and assigning a value to it with the let
keyword is a
statement. In the following example, let y = 6;
is a statement.
fn main() {
let y = 6;
}
Statements do not return values and can therefore not be assigned to another variable.
Expressions do evaluate to something. Consider a simple math operation 5 + 6
,
which is an expression that evaluates to 11
. Expressions can be part of a
statement, as can be seen in the example above. The expression 6
is assigned
to the variable y
. Calling a function is also an expression.
The body of a function is just a block. In Mun, not just bodies, but all blocks
evaluate to the last expression in them. Blocks can therefore also be used on
the right hand side of a let
statement.
fn foo() -> i32 {
let bar = {
let b = 3;
b + 3
};
// `bar` has a value 6
bar + 3
}
Returning Values from Functions
Functions can return values to the code that calls them. We don't name return
values in the function declaration, but we do declare their type after an arrow
(->
). In Mun, a function implicitly returns the value of the last expression
in the function body. You can however return early from a function by using the
return
keyword and specifying a value.
fn five() -> i32 {
5
}
fn main() {
let x = five();
}
There are no function calls or statements in the body of the five
function,
just the expression 5
. This is perfectly valid Mun. Note that the return type
is specified too, as -> i32
.
Whereas the last expression in a block implicitly becomes that blocks return
value, explicit return
statements always return from the entire function:
fn foo() -> i32 {
let bar = {
let b = 3;
return b + 3;
};
// This code will never be executed
return bar + 3;
}
Control flow
Executing or repeating a block of code only under specific conditions are common
constructs that allow developers to control the flow of execution. Mun provides
if
/else
expressions and loops.
if
expressions
An if
expression allows you to branch your code depending on conditions.
fn main() {
let number = 3;
if number < 5 {
number = 4;
} else {
number = 6;
}
}
All if
expressions start with the keyword if
, followed by a condition. As
opposed to many C-like languages, Mun omits parentheses around the condition.
Only when the condition is true - in the example, whether the number
variable
is less than 5 - the consecutive code block (or arm) is executed.
Optionally, an else
expression can be added that will be executed when the
condition evaluates to false. You can also have multiple conditions by combining
if
and else
in an else if
expression. For example:
fn main() {
let number = 6;
if number > 10 {
// The number if larger than 10
} else if number > 8 {
// The number is larger than 8 but smaller or equal to 10
} else if number > 2 {
// The number is larger than 2 but smaller or equal to 8
} else {
// The number is smaller than- or equal to 2.
}
}
Using if
in a let
statement
The if
expression can be used on the right side of a let
statement
just like a block:
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
Depending on the condition, the number
variable will be bound to the value of
the if
block or the else
block. This means that both the if
and else
arms need to evaluate to the same type. If the types are mismatched the compiler
will report an error.
loop
expressions
A loop
expression can be used to create an infinite loop. Breaking out of the
loop is done using the break
statement.
fn main() {
let i = 0;
loop {
if i > 5 {
break;
}
i += 1;
}
}
Similar to if
/else
expressions, loop
blocks can have a return value that
can be returned through the use of a break
statement.
fn count(i: i32, n: i32) -> i32 {
let loop_count = 0;
loop {
if i >= n {
break loop_count;
}
loop_count += 1;
}
}
All break
statements in a loop
must have the same return type.
let a = loop {
break 3;
break; // expected `{integer}`, found `nothing`
};
while
expressions
while
loops execute a block of code as long as a condition holds. A while
loop starts with the keyword while
followed by a condition expression and a
block of code to execute upon each iteration. Just like with the if
expression, no parentheses are required around the condition expression.
fn main() {
let i = 0;
while i <= 5 {
i += 1;
}
}
A break
statement inside the while
loop immediately exits the loop.
Unlike a loop
expression, a break
in a while loop cannot return a value
because a while loop can exit both through the use of a break
statement and
because the condition no longer holds. Although we could explicitly return a
value from the while
loop through the use of a break
statement it is unclear
which value should be returned if the loop exits because the condition no longer
holds.
extern
functions
Extern functions are declared in Mun but their function bodies are defined externally. They behave exactly the same as regular functions but their definitions have to be provided to the runtime when loading a Mun library. Failure to do so will result in a runtime link error, and loading the library will fail. Take this code for example:
extern fn random() -> i64;
pub fn random_bool() -> bool {
random() % 2 == 0
}
The random
function is marked as an extern
function, which means that it
must be provided to the runtime when loading this library.
First building the above code as main.munlib
and then trying to load the
library in Rust using:
extern crate mun_runtime;
use mun_runtime::{invoke_fn, RetryResultExt, RuntimeBuilder};
use std::{cell::RefCell, rc::Rc};
fn main() {
let runtime = RuntimeBuilder::new("main.munlib")
.spawn()
.expect("Failed to spawn Runtime");
let result: bool = invoke_fn!(runtime, "random_bool").unwrap();
println!("random bool: {}", result);
}
will result in an error:
Failed to link: function `random` is missing.
This indicates that we have to provide the runtime with the random
method,
which we can do through the use of the insert_fn
method. Let's add a method
that uses the current time as the base of our random
method:
extern crate mun_runtime;
use mun_runtime::{invoke_fn, RetryResultExt, RuntimeBuilder};
use std::{cell::RefCell, rc::Rc};
extern "C" fn random() -> i64 {
let result = std::time::Instant::now().elapsed().subsec_nanos() as i64;
println!("random: {}", result);
result
}
fn main() {
let runtime = RuntimeBuilder::new("main.munlib")
.insert_fn("random", random as extern "C" fn() -> i64)
.spawn()
.expect("Failed to spawn Runtime");
let result: bool = invoke_fn!(runtime, "random_bool").unwrap();
println!("random_bool: {}", result);
}
Note that we have to explicitly cast the function random
to extern "C" fn() -> i64
. This is because each function in Rust has its own unique type.
When we run this now, the error is gone and you should have a function that returns a random boolean in Mun.
Structs
A struct
- or structure - is a custom data type that groups related values together into a
named data structure. In this chapter we'll compare the two types of supported structures,
demonstrate how to use them, and how Mun's hot reloading works for structures.
Records vs Tuples
Mun supports two types of structures: record structs and tuple structs. A record struct
definition specifies both the name and type of each piece of data, allowing you to retrieve the
field by name. For example, Listing 3-1 shows a record struct
that stores a 2-dimensional
vector.
struct Vector2 {
x: f32,
y: f32,
}
In contrast, tuple struct
definitions omit field names; only specifying the field types. Using a tuple struct
makes sense when you want to associate a name with a tuple or distinguish it from
other tuples' types, but naming each field would be redundant. Listing 3-2 depicts a tuple struct
that stores a 3-dimensional vector.
struct Vector3(f32, f32, f32)
Create a Struct Instance
To use a record struct
, we create an instance of that struct by stating the name of the
struct
and then add curly braces containing key: value
pairs for each of its fields. The keys
have to correspond to the field names in the struct
definition, but can be provided in any order.
Let's create an instance of our Vector2
, as illustrated in Listing 3-3.
struct Vector2 {
x: f32,
y: f32,
}
let xy = Vector2 {
x: 1.0,
y: -1.0,
};
To create an instance of a tuple struct
, you only need to state the name of the struct
and
specify a comma-separated list of values between round brackets - as shown in Listing 3-4. As
values are not linked to field names, they have to appear in the order specified by the struct
definition.
struct Vector3(f32, f32, f32)
let xyz = Vector3(-1.0, 0.0, 1.0);
Field Init Shorthand
It often makes sense to name function variables the same as the fields of a record struct
.
Instead of having to repeat the x
and y
field names, the field init shorthand syntax
demonstrated in Listing 3-5 allows you to avoid repetition.
struct Vector2 {
x: f32,
y: f32,
}
pub fn vector2_new(x: f32, y: f32) -> Vector2 {
Vector2 { x, y }
}
Access Struct Fields
To access a record's fields, we use the dot notation: vector.x
. The dot notation can be used both
to retrieve and to assign a value to the record's field, as shown in Listing 3-6. As you can see,
the record's name is used to indicate that the function expects two Vector2
instances as function
arguments and returns a Vector2
instance as result.
struct Vector2 {
x: f32,
y: f32,
}
pub fn vector2_add(lhs: Vector2, rhs: Vector2) -> Vector2 {
lhs.x += rhs.x;
lhs.y += rhs.y;
lhs
}
A tuple struct
doesn't have field names, but instead accesses fields using indices - starting
from zero - corresponding to a field's position within the struct definition (see Listing 3-7).
struct Vector3(f32, f32, f32)
pub fn vector3_add(lhs: Vector3, rhs: Vector3) -> Vector3 {
lhs.0 += rhs.0;
lhs.1 += rhs.1;
lhs.2 += rhs.2;
lhs
}
Unit Struct
Sometimes it can be useful to define a struct
without any fields. These so-called unit structs
are defined using the struct
keyword and a name, as shown in Listing 3-8.
struct Unit;
Struct Memory Kind
By default, Mun is a garbage collected language. This means that memory is allocated on the heap and automatically freed by the Mun Runtime when your memory goes out of scope. Sometimes this behavior is undesired, and you want to manually control when a value is freed.
Mun allows you to specify this so-called memory kind in a struct
definition: gc
for garbage
collection or value
to pass a struct
by value; defaulting to gc
when neither is specified.
Listing 3-9 shows the previously created struct definition of a Vector2
, which has the default
gc
memory kind.
struct Vector2 {
x: f32,
y: f32,
}
To manually specify the memory kind, add round brackets containing either gc
or value
after the
struct
keyword, as illustrated in Listing 3-10.
struct(value) Vector2 {
x: f32,
y: f32,
}
Marshalling
When embedding Mun in other languages, you will probably want to retrieve, modify and send structures across the boundary - of the two languages. When this so-called marshalling occurs, there is often an associated performance penalty because the Mun Runtime needs to perform runtime checks to validate the provided data types.
Mun provides a homogeneous interface for marshalling any struct through a StructRef
- a reference
to a heap-allocated struct. The Mun Runtime automatically handles the conversion from a function
return type into a StructRef
and function arguments into Mun structs.
For structs with the
gc
memory kind, marshalling reuses the memory allocated by the garbage collector, but for structs with thevalue
memory kind this requires their value to be copied into heap memory.
Listing 3-11 shows how to marshal Vector2
instances from Mun to Rust and vice versa, using the
vector2_new
and vector2_add
functions - previously defined.
extern crate mun_runtime;
use mun_runtime::{invoke_fn, RuntimeBuilder, StructRef};
use std::{cell::RefCell, env, rc::Rc};
fn main() {
let lib_path = env::args().nth(1).expect("Expected path to a Mun library.");
let runtime = RuntimeBuilder::new(lib_path)
.spawn()
.expect("Failed to spawn Runtime");
let a: StructRef = invoke_fn!(runtime, "vector2_new", -1.0f32, 1.0f32).unwrap();
let b: StructRef = invoke_fn!(runtime, "vector2_new", 1.0f32, -1.0f32).unwrap();
let added: StructRef = invoke_fn!(runtime, "vector2_add", a, b).unwrap();
}
Accessing Fields
The API of StructRef
consists of three generic methods for accessing fields: get
, set
, and
replace
; respectively for retrieving, modifying, and replacing a struct field. The desired
field is specified using a string field_name
parameter, which is identical to the one used with
the dot notation in Mun code.
extern crate mun_runtime;
use mun_runtime::{invoke_fn, RuntimeBuilder, StructRef};
use std::{cell::RefCell, env, rc::Rc};
fn main() {
let lib_path = env::args().nth(1).expect("Expected path to a Mun library.");
let mut runtime =
RuntimeBuilder::new(lib_path)
.spawn()
.expect("Failed to spawn Runtime");
let mut xy: StructRef = invoke_fn!(runtime, "vector2_new", -1.0f32, 1.0f32).unwrap();
let x: f32 = xy.get("x").unwrap();
xy.set("x", x * x).unwrap();
let y = xy.replace("y", -1.0f32).unwrap();
}
Hot Reloading Structs
To understand how we might use hot reloading of structures, let's create the
skeleton for a simulation, as shown in Listing 3-13. The new_sim
function
constructs a SimContext
, which maintains the simulation's state, and the
sim_update
function will be called every frame to update the state of
SimContext
. As Mun doesn't natively support logging, we'll use the extern
function log_f32
to log values of the f32
type.
The subject of our simulation will be buoyancy; i.e. the upward force exerted by a fluid on a (partially) immersed object that allows it to float. Currently, all our simulation does it to log the elapsed time, every frame.
Filename: buoyancy.mun
extern fn log_f32(value: f32);
struct SimContext;
pub fn new_sim() -> SimContext {
SimContext
}
pub fn sim_update(ctx: SimContext, elapsed_secs: f32) {
log_f32(elapsed_secs);
}
To be able to run our simulation, we need to embed it in a host language. Listing 3-14 illustrates how to do this in Rust.
extern crate mun_runtime;
use mun_runtime::{invoke_fn, RetryResultExt, RuntimeBuilder, StructRef};
use std::{env, time};
extern "C" fn log_f32(value: f32) {
println!("{}", value);
}
fn main() {
let lib_dir = env::args().nth(1).expect("Expected path to a Mun library.");
let runtime = RuntimeBuilder::new(lib_dir)
.insert_fn("log_f32", log_f32 as extern "C" fn(f32))
.spawn()
.expect("Failed to spawn Runtime");
let ctx: StructRef = invoke_fn!(runtime, "new_sim").wait();
let mut previous = time::Instant::now();
const FRAME_TIME: time::Duration = time::Duration::from_millis(40);
loop {
let now = time::Instant::now();
let elapsed = now.duration_since(previous);
let elapsed_secs = if elapsed < FRAME_TIME {
std::thread::sleep(FRAME_TIME - elapsed);
FRAME_TIME.as_secs_f32()
} else {
elapsed.as_secs_f32()
};
let _: () = invoke_fn!(runtime, "sim_update", ctx.clone(), elapsed_secs).wait();
previous = now;
runtime.borrow_mut().update();
}
}
Now that we have a runnable host program, let's fire it up and see that hot reloading magic at work! First we need to start the build watcher:
mun build buoyancy.mun --watch
This will create the initial buoyancy.munlib that we can use to run our host program in Rust:
cargo run -- buoyancy.munlib
Your console should now receive a steady steam of 0.04... lines, indicating that the simulation is indeed running at 25 Hz. Time to add some logic.
Insert Struct Fields
Our simulation will contain a spherical object with radius r and density
do that is dropped from an initial height h into a body of water
with density dw. The simulation also takes the gravity, g, into
account, but for the sake of simplicity we'll only consider vertical movement.
Let's add this to the SimContext
struct and update the new_sim
function
accordingly, as shown in Listing 3-15.
struct SimContext {
sphere: Sphere,
water: Water,
gravity: f32,
}
struct Sphere {
radius: f32,
density: f32,
height: f32,
velocity: f32,
}
struct Water {
density: f32,
}
pub fn new_sim() -> SimContext {
SimContext {
sphere: new_sphere(),
water: new_water(),
gravity: 9.81,
}
}
fn new_sphere() -> Sphere {
Sphere {
radius: 1.0,
density: 250.0,
height: 1.0,
velocity: 0.0,
}
}
fn new_water() -> Water {
Water {
density: 1000.0,
}
}
Runtime Struct Field Initialization
Upon successful compilation, the runtime will hot reload the new structs. Memory of newly added structs will recursively be zero initialized. This means that all fundamental types of a newly added structs and its child structs will be equal to zero.
We can verify this by replacing the log_f32(elapsed_secs)
statement with:
log_f32(ctx.gravity);
Indeed the console now receives a stream of 0
lines. Luckily there is a trick
that we can employ to still manually initialize our memory to desired values by
using this behaviour to our advantage. Let's first add token: u32
to the
SimContext
:
token: u32,
and set it to zero in the new_sim
function:
token: 0,
As before, the token
value will be initialized to zero when the library has
been hot reloaded. Next, we add a hot_reload_token
function that returns a
non-zero u32
value, e.g. 1
:
fn hot_reload_token() -> u32 {
1
}
Finally, we add this if
statement to the sim_update
function:
if ctx.token != hot_reload_token() {
let default = new_sim();
ctx.sphere = default.sphere;
ctx.water = default.water;
ctx.gravity = default.gravity;
ctx.token = hot_reload_token();
}
This piece of code will be triggered every time the hot_reload_token
function
returns a different value, but only once - allowing us to initialize the value
of SimContext
.
Edit Struct Fields
Time to add the actual logic for simulating buoyancy. The formula for calculating the buoyancy force is force = submerged volume * water density * gravity.
fn calc_submerged_ratio(s: Sphere) -> f32 {
let bottom = s.height - s.radius;
let diameter = 2.0 * s.radius;
if bottom >= 0.0 {
0.0
} else if bottom <= -diameter {
1.0
} else {
-bottom / diameter
}
}
fn calc_sphere_volume(radius: f32) -> f32 {
let pi = 3.1415926535897;
let r = radius;
3.0/4.0 * pi * r * r * r
}
fn calc_buoyancy_force(s: Sphere, w: Water, gravity: f32, submerged_ratio: f32) -> f32 {
let volume = calc_sphere_volume(s.radius);
volume * submerged_ratio * w.density * gravity
}
Next we need to convert force into acceleration using acc = force / mass. We
don't readily have the sphere's mass available, but we can derive it using the
sphere's volume and density: mass = volume * density. Instead of doing this
every frame, let's replace the sphere's density
field with a mass
field:
struct Sphere {
radius: f32,
mass: f32, // density: f32,
height: f32,
velocity: f32,
}
and pre-calculate it on construction:
fn new_sphere() -> Sphere {
let radius = 1.0;
let density = 250.0;
let volume = calc_sphere_volume(radius);
let mass = density * volume;
Sphere {
radius,
mass,
height: 1.0,
velocity: 0.0,
}
}
To initialize the sphere's mass
field, we can employ the same trick as before;
this time only initializing the sphere and incrementing hot_reload_token
to
2
:
if ctx.token != hot_reload_token() {
let default = new_sphere();
ctx.sphere = default;
ctx.token = hot_reload_token();
}
Editing a field's name is only one of three ways that you can edit struct fields in Mun. In order of priority, these are the changes that the Mun Runtime is able to detect:
- If an old field and new field have the same name and type, they must have remained unchanged. In this case, the field can be moved.
- If an old field and new field have the same name, they must be the same field. In this case, we accept a type conversion and the field can potentially be moved.
- If an old field and new field have different names but the same type, the field could have been renamed. As there can be multiple candidates with the same type, we accept the renamed and potentially moved field that is closest to the original index of the old field.
Some restrictions do apply:
- A struct cannot simultaneously be renamed and its fields edited.
- A struct field cannot simultaneously be renamed and undergo a type conversion.
In both of the above cases, the difference will be recognised as two separate changes: an insertion and a deletion of the struct/field.
Remove Struct Fields
We now have all of the building blocks necessary to finish our buoyancy simulation. If the sphere is (partially) submerged, we calculate and add the buoyancy acceleration to the velocity. We also always subtract the gravitational acceleration from the velocity to ensure that the sphere drops into the water.
One important thing to take into account when running simulations is to multiply the accelerations and velocities with the elapsed time, as we are working in discrete time.
Last but not least, let's log the sphere's height to the console, so we can verify that the simulation is running correctly.
let submerged_ratio = calc_submerged_ratio(ctx.sphere);
if submerged_ratio > 0.0 {
// Accelerate using buoyancy
let buoyancy_force = calc_buoyancy_force(
ctx.sphere,
ctx.water,
ctx.gravity,
submerged_ratio
);
let buoyancy_acc = buoyancy_force / ctx.sphere.mass;
ctx.sphere.velocity += buoyancy_acc * elapsed_secs;
}
// Accelerate using gravity
ctx.sphere.velocity -= ctx.gravity * elapsed_secs;
// Apply velocity
ctx.sphere.height += ctx.sphere.velocity * elapsed_secs;
log_f32(ctx.sphere.height);
When the simulation has been hot reloaded, the console should now log height values of the ball that are indicative of a sphere bobbing on the waves.
Now that our simulation is completed, we no longer need the token
field,
hot_reload_token
function, and if
statement. The token
field can be
safely removed and the simulation hot reloaded without losing any state.
Well done! You've just had your first experience of hot reloading strucs.