"Rust bindings for the Matlab C API"

Overview

matlab-sys

matlab-sys provides low level bindings to Matlab's C API. This allows writing MEX functions, using the C Matrix API, interacting with the C MAT API and authoring Matlab Engine Applications using Rust.
The bindings are mostly automatically generated by bindgen and named the same or similar as in the C headers. Some manual changes are made to the bindings to ease usage or simplify the exposed API without sacrificing functionality, such as replacing type definitions for the fixed size integer types with the native fixed size types from Rust.

Usage

To make use of the functionality provided by this crate it has to be linked against the library files libmex.lib, libmx.lib, libmat.lib and libeng.lib provided by your installation of Matlab. If the environment variable MATLABPATH is set to the directory of the Matlab installation to link against, this path will be used preferentially. Otherwise the build script will attempt to seek the installation directory of the default matlab installation by running the following command on the build machine: matlab -batch "disp(matlabroot)". This usually takes a few seconds for Matlab to start up, so explicitly providing the path in the aforementioned environment variable helps compile times and should be preferred.

If you have special requirements with regard to the linking process which can not be handled by the approach described above, the necessary link directives can be specified in a .config file with the rustc-link-lib and rustc-link-search keys. Further information can be found here.

Building MEX functions directly

To build a mex function in Rust the crate type has to be a dynamic link library with a C ABI. In your cargo.toml set crate-type = ["cdylib"]. Every MEX function, regardless what name it will be called with from Matlab, needs an entry point with the following signature:

#[no_mangle]
pub unsafe extern "C" fn mexFunction(
    nlhs: c_int,
    plhs: *mut *mut mxArray,
    nrhs: c_int,
    prhs: *const *const mxArray,
){/* your calculation */}

After building, change the file extension of the build artifact to *.mexw64 for windows or *.mexa64 for linux. Copy the renamed file somewhere where Matlab can find it. You should now be able to call your mex function using the filename without its extension.

Examples

A quick and simple translation of the arrayProduct example, which multiplies a scalar with a matrix (without most error handling for brevity) would look like this:

use matlab_sys::interleaved_complex as raw;
use std::ffi::{c_int, CString};
#[no_mangle]
pub unsafe extern "C" fn mexFunction(
    nlhs: c_int,
    plhs: *mut *mut raw::mxArray,
    nrhs: c_int,
    prhs: *const *const raw::mxArray,
) {
    if nrhs != 2 {
        // Letting the standard library do the work of making Rusts strings C-compatible
        raw::mexErrMsgIdAndTxt(
            CString::new("MyToolbox:arrayProduct:nrhs").unwrap().as_ptr(), 
            CString::new("Two inputs required.").unwrap().as_ptr()
        );
    }
    if nlhs != 1 {
        // Writing C-compatible strings manually
        raw::mexErrMsgIdAndTxt(
            b"MyToolbox:arrayProduct:nlhs\0".as_ptr().cast(), 
            b"One output required.\0".as_ptr().cast()
        );
    }
    // SAFETY: nlhs and nrhs should get verified to be greater or equal to 0 and all pointers should get tested for validity
    let plhs = std::slice::from_raw_parts_mut(plhs, nlhs as usize);
    let prhs = std::slice::from_raw_parts(prhs, nrhs as usize);

    /* get the value of the scalar input  */
    let multiplier = raw::mxGetScalar(prhs[0]);

    /* create a pointer to the real data in the input matrix  */
    let inMatrix = raw::mxGetDoubles(prhs[1]);

    /* get dimensions of the input matrix */
    let ncols = raw::mxGetN(prhs[1]);

    /* create the output matrix */
    plhs[0] = raw::mxCreateDoubleMatrix(1, ncols, raw::mxComplexity::mxREAL);

    /* get a pointer to the real data in the output matrix */
    let outMatrix = raw::mxGetDoubles(plhs[0]);

    /* call the computational routine */
    array_product(multiplier, inMatrix, outMatrix, ncols);
}

unsafe fn array_product(x: f64, y: *mut f64, z: *mut f64, n: usize) {
    unsafe {
        for i in 0..n {
            *z.add(i) = x * *y.add(i);
        }
    }
}

More examples can be found in the mex-examples directory. These examples are direct translations of the original C examples as distributed with Matlab in matlabroot/extern/examples. They are also used as tests for the bindings, as such they should always compile and work.

Building abstractions on top of matlab-sys

While the ability of Rust to interact with C libraries is nice, its true power lies in its ability to build safe, ergonomic, easy-to-use and hard-to-misuse abstractions. This crate in intended to work as a common building block for those abstractions in the context of Matlab extensions.
If you happen to work on such an abstraction please let me know! I would love to know how my work is being used and to promote your crate here.

Features

matlab-sys exposes both the older separate-complex C API which stands to be deprecated by Mathworks in the future as well as the newer interleaved-complex C API, which is set to become the default in Matlab instead. While most functions present in the separate-complex API are also present in the interleaved-complex API, there are some important differences. The interleaved-complex API for instance provides new convenient and safe(er) typed data access functionality. Most importantly though, some types have a different representation (e.g. mxArray of complex numbers) and some functions behave differently even though the API stayed the same (e.g. mxGetData). To minimize the resulting risk of confusion between both versions of the API, matlab-sys exposes them in separate namespaces.

Do not use functions of one namespace with types of the other one. Since the namespace is part of the type, Rust's type system helps to prevent this issue. To further prevent this issue it is recommended to activate only the API you intend to use in your crate using the corresponding features. More information about the differences of the APIs can be found here.

In Matlab it is impossible to enable both versions of the API at the same time when using the mex command, which mitigates the danger of mixing APIs. While this mutually exclusive API could also be achieved in this crate by using mutually exclusive features, this would lead to some significant drawbacks. The dependency resolver of cargo unifies all features of a crate which are enabled by any dependent crate in the dependency tree, effectively compiling the crate with the union of all enabled features in the project. In the case of mutually exclusive features this would lead to the inability for multiple separate intermediate dependencies to use the different APIs, even internally. By keeping them in separate namespaces, the features to enable them satisfy cargo's requirement for features to be purely additive.

separate-complex

This feature enables the older API version and corresponds to the API available when compiling with Matlab's mex command without additional arguments or with the additional argument -R2017b. As the name implies arrays of complex numbers are represented internally by separate arrays for the real and the imaginary part. This is the only available option for Matlab versions R2017b and prior. For all later releases Matlab uses the interleaved-complex representation internally. Using this API with Matlab releases after R2017b will incur potentially unnecessary copying when translation to the separate complex representation happens. In addition many new features of the newer API, such as typed data access, are not available.

interleaved-complex - default

This feature enables the newer API version and corresponds to the API available when compiling with Matlab's mex command with the additional argument -R2018a. This API is set to become the new default API for compilation with Matlab's mex command in the future. Arrays of complex numbers are represented internally by a single array with interleaved real and imaginary parts. This is the representation Matlab uses internally since release R2018a. As a result no copying has to take place at the interface. This representation is also used by most popular numeric libraries, further reducing the need for copying or transformations. Using this feature with a Matlab release older than R2018a will not work! For all newer releases this is the recommended API.

You might also like...
Comments
  • MacOS support

    MacOS support

    These changes feel a bit hacky, but it seems like you need to remove the \u{1} from the autogenerate bindgen code to make it work on all platforms. I tested this on MacOS, Windows 10 and Linux (Ubuntu 20.04) and it all seems to work and function as expected. I ran the xtask test and it seemed to work just fine.

    To be honest I'm not sure if this is the correct solution, or if there is a bindgen setting that will always generate universal bindings or generate those bindings at compile time?

    enhancement 
    opened by avikagan 2
  • Improve testing by translating the Matlab C API examples to Rust using matlab-sys

    Improve testing by translating the Matlab C API examples to Rust using matlab-sys

    Improve testing

    Currently only a small subset of the whole Matlab C API gets tested using the subset of examples translated to Rust which can be found in ./mex-examples. While this already validates the linking directives needed to link against libraries provided by Matlab it would be better if a bigger surface of the exposed C API could be tested to make sure the bindings work correctly and nothing is missing.

    Workflow

    1. Pick a C-example provided by Matlab in matlabroot/extern/examples/ that is missing
    2. Make a new cdylib library crate in ./mex-examples with the name of the example (words should be separated by '-' for consistency) and matlab-sys as a path dependency
    3. Translate the C-example you picked while staying close to the structure of the C-example
    4. Add the exact name of the translated example package to TEST_EXAMPLES in file ./xtask/src/test.rs
    5. Add at least one matlab unit test for the translated example to the file ./xtask/src/test.m. Follow the examples provided by the other tests or the documentation from Matlab.
    6. Run the tests (cargo xtask test) to make sure everything works. If the new test fails validate your translation of the C-example and the test case you added to test.m. If both are correct open an issue describing the problem.
    enhancement help wanted good first issue 
    opened by Tastaturtaste 0
Owner
null