Expand description

Rustmex

A library providing convenient Rust bindings to Matlab’s MEX C api.

Rustmex makes writing MEX functions in Rust a bit easier. It convert Matlab types, and the arguments it provides to mexFunction into more Rusty types, which can then be used to interface with other Rust code easily.

In this readme:

  1. Installation
  2. Usage and Examples
  3. Design and Internals
  4. Future plans and TODO
  5. Licence

Installation

The installation of this crate is slightly more involved than you might be used to: you need to select a target API. This target API depends on what version of Matlab or GNU/Octave you are targeting with your library. See the API section for that. Once you’ve selected your target API, put the following in your Cargo.toml:

rustmex = { version = "0.4.1", features = ["matlab_interleaved"] }

where "matlab_interleaved" is then one of the options for target APIs.

Furthermore, if want to use rustmex to write a MEX file, you need to add the following to your Cargo.toml file too:

[lib]
crate-type = ["cdylib"]

Compiling your crate then results in a C dynamic library, instead of a Rust library.

Compilation and linkage

On some platforms with some target API’s (in particular Windows with Matlab; your milage may vary with Octave), you might need to tell the linker where some libraries are located.

On those platforms, Rustmex’s build script unconditionally fails if you don’t tell the linker where the libraries are located. Otherwise you will run into one impenetrable error, while with the build script failing you get a nice error message.

You tell the linker where the libraries are located by overriding the build script In a config.toml like the following:

[target.<triple>.mex]
rustc-link-search = ["~/bin/R2022a/bin/glnxa64"]
rustc-link-lib = ["mx", "mex", "mat"]

you set the appropriate triplet, library search path, and libraries to link. Substitute <triple> with the triple you are compiling for (the “host” field of the output of rustc -vV). Matlab will print the appropriate search path when you run fullfile(matlabroot(), 'bin', computer('arch')) in your Matlab prompt.

If you are on another platform than Matlab on Windows, and the compilation still fails with a linker error, you might also need to do this process there. Create an empty C file test.c for example, and “dry run” the creation of a mex file from it. For matlab, you can do this with mex -n test.c, for Octave with mkoctfile -n test.c --mex. Look, on the second line, for arguments starting with -L and -l, and substitute these in the config.toml file for the link search path and link library path respectively.

Usage

Each MEX function file has an entrypoint called mexFunction. This is a C FFI function, which, preferably, you do not want to write yourself.

Instead, rustmex provides the entrypoint macro; a macro to mark your Rust entrypoint with. For example:

use rustmex::prelude::*;

#[rustmex::entrypoint]
fn hello_world(lhs: Lhs, rhs: Rhs) -> rustmex::Result<()> {
	println!("Hello Matlab!");
	Ok(())
}

Note that this example mirrors the definition of mexFunction itself: Matlab has already allocated a return value array, you just need to place your results into it.

The FromMatlabError is for when you want to convert an mxArray into a more Rusty data type. These conversions are not infallible: Matlab is dynamically typed, and the provided mxArray must have the same type and size as the type we want to convert it into.

As a more convoluted example:

use rustmex::prelude::*;

#[rustmex::entrypoint]
fn euclidean_length(lhs: Lhs, rhs: Rhs) -> rustmex::Result<()> {
	let v: &[f64] = rhs
		.get(0)
		.error_if_missing("euclidean_length:missing_input",
			"Missing input vector to compute the length of")?
		.data_slice()?;

	// I recommend to only execute your planned computation once you're sure it
	// actually needs to be computed — it's a bit of a waste to compute an expensive
	// result without somewhere to return it to.
	if let Some(ret) = lhs.get_mut(0) {
		ret.replace(v.iter().map(|x|x*x).sqrt().to_matlab());
	}
	Ok(())
}

This example computes the Euclidean length of an input vector. Note that type annotations are (almost always) needed for the return type of data_slice() and from_matlab().

Regarding FromMatlab, this library also supports building NDarrays from mxArrays. It will ensure that NDarray understands the data the same way matlab does. For example, the following prints out a debug representation of the array:

use rustmex::prelude::*;
use ndarray::ArrayViewD;

#[rustmex::entrypoint]
fn display_matrix(lhs: Lhs, rhs: Rhs) -> rustmex::Result<()> {
	if let Some(mx) = rhs.get(0) {
		let mat = ArrayViewD<f64>::from_matlab(mx)?;
		eprintln!("{mat:#?}");
	}
	Ok(())
}

Calling back into Matlab is also supported. For example, to compute the square root of the sum of squares (i.e. nd-pythagoras):

% Call Rust MEX file
x = rusty_fn_call(@(x) sqrt(sum(x.^2)), 1:10)
use rustmex::prelude::*
use rustmex::function::Function;

#[rustmex::entrypoint]
fn rusty_fn_call(lhslice: rustmex::Lhs, rhslice: rustmex::Rhs) -> rustmex::Result<()> {

	let f = rhslice
		.get(0)
		.error_if_missing("rusthello:no_fn", "Didn't get a function")?
		.to_rust::<Function<_>>()?;

	if let Some(r) = lhslice.get_mut(0) {
		// Just forward the remaining arguments
		let mut results: Box<[Option<MxArray>; 1]> = f.call(&rhslice[1..]).unwrap();

		let v = results[0].take().unwrap();

		r.replace(v);
	};

	return Ok(());
}

prints:

x =  19.621

If you assume something about the data you receive, but it might not yet be in the right shape, you might want to reshape. Rustmex enables ergonomically reshaping data: you can apply the ? operator on a Result with a ShapeError in functions which return a rustmex::Result:

use rustmex::prelude::*;
use ndarray::{ArrayViewD, Ix2};

#[rustmex::entrypoint]
fn display_matrix(lhs: Lhs, rhs: Rhs) -> rustmex::Result<()> {
	if let Some(mx) = rhs.get(0) {
		let mat = ArrayViewD<f64>::from_matlab(mx)?
			.into_dimensionality<Ix2>()?
			.into_shape((1, 4))?;
		eprintln!("{mat:#?}");
	}
	Ok(())
}

Design and Internals

Writing MEX extensions is not as straightforward as you might be useful. In particular, Matlab (by default) breaks some fundamental assumptions Rust makes on memory management. do not skip over the memory management section.

Ergonomics

The design of the library consists of three levels:

  1. The Raw FFI level: the C FFI definitions
  2. The wrapped mex level: functionality wrapping the raw level for a more ergonomic and safe Rust interface
  3. Top level: Wrapping the mex level, providing easy conversions to and from mxArrays.

It is recommended to write against the level which supports your features. If Rustmex does not implement functionality you need, the lower levels are still available for that.

APIs

Rustmex wraps the bindings exposed by Matlab (pre and post 2017b) and GNU/Octave. The features these bindings provide are all slightly different, so select one via a cargo feature. These are:

  • matlab_interleaved: Matlab, release 2018a and later, with the interleaved complex API;
  • matlab_separated: Matlab, release 2017b and earlier, with the separate real and complex parts of mxArrays;
  • octave: for GNU/Octave. (uses the same representation as matlab_separated)

If you’ve compiled your crate as a cdylib, you can then take the resultant dynamic library (on linux a .so), copy it to the target location in your Matlab path, with the appropriate file extension. (.mexa64 for MEX on 64 bit Linux, .mexw64 for MEX on Windows, and .mex for GNU/Octave.)

Some of these targets also use symbol versioning, so e.g. mxCreateNumericArray is actually mxCreateNumericArray_800 for the matlab_interleaved API. For ease of use, rustmex renames (pub use $x_$v as $x) these symbols to all have the same name in mex::raw.

When Rustmex is compiled for an API which represents complex values with two arrays, slightly different implementations for data conversions are used. Since most Rust code assumes that complex values are interleaved (i.e., the real and imaginary parts of the value are stored next to eachother in memory) this might cause a lot of copying on the interface.

See also https://nl.mathworks.com/help/matlab/matlab_external/matlab-support-for-interleaved-complex.html.

Memory Management

The Mex API has a few quirks when it comes to memory management. In particular, allocations made via mxMalloc are, by default, deallocated when the MEX function returns. This causes problems when some part of Rust code assumes that they are responsible for deallocating the memory and assuming it is still after. Rustmex therefore marks all allocations as persistent via mexMakeMemoryPersistent. This may cause your MEX function to leak instead of crash (the latter takes Matlab with it). See also mex::alloc::mxAlloc.

TODO

  • Structs and Objects
    • From/To Array<HashMap<Field, Value>>
    • From/To Rust struct via proc-macro?
  • Other conversion targets
    • Support matlab character arrays, strings
    • Nalgebra?
  • Better error handling
    • Ergonomically return some error trait object.
    • Compose well with other error handling libraries; break down elegantly via std::Error
  • Wrapping MEX functionality
    • mexAtExit (also important for dropping static values.
    • mexLock/mexUnlock
    • mexGetVariablePtr/mexGetVariable/mexPutVariable
    • Calling back into Matlab

Licence

This is licensed to you under the Mozilla Public License, version 2. You can the licence in the LICENCE file in this project’s source tree root folder.

Authors

  • Niels ter Meer (maintainer)

Re-exports

pub use mex::raw::mxArray;
pub use mex::FromMatlabError;
pub use mex::ToMatlabError;
pub use mex::pointers::MxArray;
pub use message::MexMessage;
pub use message::Error;
pub use message::Missing;

Modules

This module contains the conversion implementations for types which can be unambiguously converted to an from an mxArray. That is, the type itself has enough information to determine how many dimensions it should have, and what the lengths of these are.

This module contains functionality to call back into Matlab. Both calling via a function_handle or a named matlab function (such as “sqrt”) are supported.

Matlab handles errors in a variety of ways, but all of them ultimately boil down to error/warning id plus a message. This module describes that way of error handling in a more Rusty way.

This module wraps the raw ffi bindings (See crate::mex::raw) exposed by each API target to something a bit more ergonomic. For example, when the MEX api exposes a pointer and a length, these are combined into a slice.

As is convention in Rust, Rustmex defines a prelude to easily import often used functionality.

Macros

Check some boolean condition; if it is not met, return an Err(Error) with the given id and message. Can be used within functions which return a rustmex::Result.

Generate a Matlab error

Check some boolean condition, if it is met, return an Err(Error) with the given id and message. Can be used within functions which return a rustmex::Result.

Structs

A complex number in Cartesian form.

Functions

Generate an ad-hoc warning from an id and a message

Type Definitions

The “left hand side” of a call to a mex function. In this slice, the return values should be placed.

Convenience type for returning a Error containing a MexMessage in the error path.

The “Right hand side” of a call to a mex function. These are the arguments provided to it in matlab.

Attribute Macros

Re-export the macro to annotate the entry point.