#mono #scripting-language #c-sharp #dotnet #framework #api-bindings #f-sharp

sys wrapped_mono

wrapped_mono is a safe, lightweight wrapper around the mono library. It allows embedding of the mono runtime inside a rust project. Inside this embedded runtime code written in languages supporting the .NET framework, such as C# and F#, can be run. This allows usage of libraries written in those languages, and using them as a scripting language. The mono runtime is used by many game engines, and this wrapper allows using it with projects written in Rust too.

3 unstable releases

0.3.1 Apr 22, 2023
0.3.0 Apr 22, 2023
0.2.0 Oct 17, 2022

#190 in Memory management

MIT license

700KB
18K SLoC

wrapped_mono

wrapped_mono is a safe, lightweight wrapper around the mono library. It allows embedding of the mono runtime inside a rust project. Inside this embedded runtime code written in languages supporting the .NET framework, such as C# and F#, can be run. This allows usage of libraries written in those languages, and using them as a scripting language. The mono runtime is used by many game engines, and this wrapper allows using it with projects written in Rust too.

WIP

Lacking APIs

While wrapped_mono already has support for most of the features of the mono runtime, some minor APIs don't have finished and fully tested wrappers. Those unfinished APIs are usually niche(eg. advanced debugging, access to pro filer(data about performance), access to assembly Metadata, dynamic code generation) and always have an alternative unsafe bindings that can be used.

Safety checks

This API tries to follow rusts rules about safety and error handling as much as possible, but some checks are unfinished and can't catch all potential problems, or are not done, since they would introduce a serious performance hit, while only covering a niche case that is clearly marked in documentation. A good example of this kind of case is accessing an object after deleting the domain it is in or shutting down the runtime. Most of possible errors are checked for, and those checks can be disabled to speed up wrapped_mono even more, but this is not advised. Cost of those checks is usually negligible(less than 1% of the cost of calling a function), and they prevent a lot of potential mistakes.

Supported platforms

wrapped_mono supports Linux(tested on Fedora 37, Debian Bullseye and Arch), and Windows(tested on Windows 10). Other platforms, such as MacOS are not officially supported, but can be easily added by changing the build.rs to include platform-specific link flags.

Dependencies

External

  • Mono library - the library this crate wraps around. Can be downloaded here. When installing, use default instructions from the website. Only needed on the system crate is compiled on (linked statically).

Rust

  • wrapped_mono_macros - sub crate containing custom macros used by wrapped_mono. Separate, because proc_macro's must be separate crates.
  • document-features - used for documentation
  • lazy_static - used to lazy-load classes such as System.Delegate when using the crate.

Features

Fully finished

  • Initialize mono runtime
  • Create different application domains to prevent code from different assemblies from interacting.
  • Load assemblies and get classes from them
  • Create new instances of a class
  • Get methods from a class - static, non-static, virtual - with signature checks
  • Call methods - static, virtual, on a specific object
  • Read and Set fields of a class - static, non-static
  • Use getter, setter and indexer properties of a class.
  • Box, unbox, clone, get hash, size, cast, convert Object to strings.
  • Raise and catch exceptions
  • Create n-dimensional Arrays, read and set their elements at any indices.
  • Pass basic types(integers, chars, floating-point numbers, pointers, arrays, strings, exceptions,objects,types,delegates) between managed and unmanaged code
  • Invoke deleagtes
  • Implement simple traits to pass any type between rust and C#/F# code!
  • Automatically implement interop helper traits for any structs made from other types implementing the helper traits.
  • Pass back and forth simple rust enums.
  • Implement C# method as internal calls to Rust code.
  • Automatically register all references to managed objects from rust code. Every reference to an object in rust code is registered, and Garbage Collector is automatically informed when it can and can't run.
  • Load custom config files.

WIP features

  • Profiler - get useful insights about how your C#/F# assembly runs - which objects are created, how often is each method called(Already supports couple different events, but contains some bugs)
  • Assembly Metadata Access - Works, but is not finished.

Planned features

  • C#/F# assembly binding generator
  • Dynamic code generation
  • Security API
  • Rust representation of System.Thread
  • Debugging features

Example

use wrapped_mono::*;
fn main(){
    // Initialise the runtime with default version(`None`), and root domian named "main_domain"
    let domain = jit::init("main_domain",None);

    // Load assembly "SomeAssembly.dll"
    let assembly = domain.assembly_open("SomeAssembly.dll").expect("Could not load assembly!");
    // Get the image, the part of assembly containing executable code(classes,methods, etc.)
    let image = assembly.get_image();
    // Get class named SomeClass in SomeNamespace
    let class = Class::from_name(&image,"SomeNamespace","SomeClass").expect("Could not find SomeClass!");
    // Create an instance of this class
    let instance = Object::new(&domain,&class);
    // Creating an instance of a class DOES NOT CALL ITS CONSTRUCTOR. The constructor is a method named '.ctor', that has to be called separately

    // Get a constructor method of SomeClass accepting an integer and a string (2 parameters)
    let ctor:Method<(i32,String)> = Method::get_from_name(&class,".ctor(int,System.String)",2).expect("Could not find the constructor!");
    // Call the constructor
    ctor.invoke(Some(instance.clone()),(12,"SomeString".to_owned())).expect("Got an exception while calling the constructor!");
    // Get a method "DoABackflip" form SomeClass with 1 parameter of type int returning a byte
    let met:Method<(i32,String)> = Method::get_from_name(&class,"DoABackflip",1).expect("Could not find method \"DoABackFlip\"!");
    // Call "DoABackflip" method on an instance
    let res_obj = met.invoke(Some(instance),(32,"Message".to_owned())).expect("Got an exception while calling DoABackflip!").expect("Got null from DoABackFlip");
    // Unbox the result to get a raw integer from a boxed integer
    let res = res_obj.unbox::<u8>();
    // Create a function with the special "invokable" attribute
    #[invokable]
    fn sqrt(input:f32)->f32{
        if input < 0.0{
            // can't get sqrt of a negative number, so create a managed exception and throw it
            unsafe{Exception::arithmetic().raise()};
        }
        input.sqrt()
    }
    // Replace a method with "[MethodImplAttribute(MethodImplOptions.InternalCall)]" atribute with a rust function
    add_internal_call!("SomeClass::SqrtInternalCall",sqrt);
    // This supports all types with `InteropRecive` trait
    #[invokable]
    fn avg(input:Array<Dim1D,f32>)->f32{
        let mut avg = 0.0;
        for i in 0..input.len(){
            let curr = input.get([i]);// get the element at index i
            avg += curr/(input.len() as f32);
          }
        avg
    }
    // Replace a method with "[MethodImplAttribute(MethodImplOptions.InternalCall)]" attribute with a rust function
    add_internal_call!("SomeClass::AvgInternalCall",sqrt);
}

Dependencies

~2MB
~42K SLoC