Sam Carey
Sam Carey

Reputation: 11

Migrate Cython driver to Rust FFI

Summary

I have some Cython code to interact with a 3rd party C++ driver library for a USB device. I am able to call the library from Python and open a device session. However, when I try to call the library from Rust using the FFI, I run into issues. Unfortunately, the 3rd party library is closed-source, so the best I can do is make sure my implementation is identical in Python and Rust.

Working Python/Cython Implementation (Python 3.9)

# main.py

from driver import Driver

def main():
    print(f"DriverVersion: {Driver.get_driver_version().decode()}")
    d = Driver()
    success = d.initialize_session()
    d.close_session()
    assert success

if __name__ == "__main__":
    main()
# driver.pxd

from libcpp cimport bool

cdef extern from "NrtControl.h":
    bool NrtOpenDevice( const char *cpPort, int *piSession )
    bool NrtCloseDevice( int iSession )
    const char * NrtGetDriverVersion()
# driver.pyx

cdef class Driver:
    cdef int iSession
    cdef char *cpComport

    def __init__(self):
        self.iSession = 0

    def initialize_session(self):
        return NrtOpenDevice("COM8", &self.iSession)

    @staticmethod
    def get_driver_version():
        return NrtGetDriverVersion()

    def close_session(self):
        NrtCloseDevice(self.iSession)
# setup.py

from distutils.core import Extension, setup

import numpy as np
from Cython.Build import cythonize
from Cython.Distutils import build_ext


def build():
    equipment_exts = [
        Extension(
            name="driver",
            sources=["driver.pyx"],
            include_dirs=[
                "C:\\Program Files\\NrtControl_for_Windows_20180921\\include",
            ],
            library_dirs=[
                "C:\\Program Files\\NrtControl_for_Windows_20180921\\VS2017\\x64\\Release"
            ],
            libraries=["NrtControl"],
            language="c++",
        )
    ]

    setup(
        cmdclass={"build_ext": build_ext},
        ext_modules=cythonize(
            equipment_exts, compiler_directives={"language_level": 3}
        ),
    )


if __name__ == "__main__":
    build()

I run the above with python setup.py build_ext --inplace and then python .\main.py and get the following output:

DriverVersion: 1.5.5.0

Failing Rust Implementation (Rust 1.60)

// Cargo.toml

[package]
name = "nrt-ffi-bindings"
version = "0.1.0"
links = "NrtControl"
build = "build.rs"
edition = "2021"

[dependencies]
libc = "0.2"
// build.rs

fn main() {
    println!("cargo:rustc-link-search=C:\\Program Files\\NrtControl_for_Windows_20180921\\VS2017\\x64\\Release");
}
// lib.rs

use libc::{c_char, c_int};

#[link(name = "NrtControl")]
extern "C" {
    #[link_name = "?NrtOpenDevice@@YA_NPEBDPEAH@Z"]
    pub fn NrtOpenDevice(cpPort: *const c_char, piSession: *mut c_int) -> bool;

    #[link_name = "?NrtCloseDevice@@YA_NH@Z"]
    pub fn NrtCloseDevice(iSession: c_int) -> bool;

    #[link_name = "?NrtGetDriverVersion@@YAPEBDXZ"]
    pub fn NrtGetDriverVersion() -> *const c_char;
}

#[cfg(test)]
mod tests {
    use std::ffi::{CStr, CString};

    use super::*;

    #[test]
    fn open_close() {
        unsafe {
            println!(
                "DriverVersion: {}",
                CStr::from_ptr(NrtGetDriverVersion()).to_str().unwrap()
            );
            let port = CString::new("COM8").unwrap();
            let session: c_int = 0;
            let success = NrtOpenDevice(port.as_ptr(), session as *mut c_int);
            NrtCloseDevice(session);
            assert!(success);
        }
    }
}

When I run the above with cargo test -- --test-threads=1, I get the following:

DriverVersion: 1.5.5.0
thread 'tests::open_close' panicked at 'assertion failed: success', src\lib.rs:32:13

Notes

C:\\Program Files\\NrtControl_for_Windows_20180921\\VS2017\\x64\\Release contains NrtControl.dll and NrtControl.lib, which were provided to me by a 3rd party.

Without the link_name attributes above, I get errors at runtime about failing to resolve external symbols. I had to run dumbin.exe on NrtControl.lib and search for the proper symbol names to copy and paste. I tried to find a way to do this with bindgen, but eventually resorted to this manual process. If anyone knows how I can avoid this, please do share.

Problem

So the problem is that my Rust invocation of the NrtOpenDevice function above returns false indicating failure, and I'm out of ideas on how to troubleshoot this.

Edit 1:

Adding NrtControl.h and nrtdef.h

Edit 2:

Following @rodrigo's suggestion below, I changed the call to:

let mut session: c_int = 0;
let success = NrtOpenDevice(port.as_ptr(), &mut session);

and it works (returns true)!

I still need to figure out how to make the link_name attributes target the correct symbol names automatically, as entering them manually is going to get tedious in the future...

Upvotes: 0

Views: 353

Answers (0)

Related Questions