Call C++ from Rust with the cpp crate

Interoperate with C++ libraries, using Qt as an example

Olivier Goffart
https://woboq.com

About Me

KDE
Nokia (Qt)
2007 - 2011
Woboq GmbH
2011 -

What is Qt?

Example C++ Qt Header


class Status : public QObject
{
  Q_OBJECT
  Q_PROPERTY(QString statusText READ statusText
             WRITE setStatusText NOTIFY statusTextChanged)
  QString m_statusText;
public:
  QString statusText() const;
public slots:
  void setStatusText(const QString &txt);
signals:
   void statusTextChanged();
};

QML


Window {
    property Status model;

    MouseArea {
        anchors.fill: parent;
        onClicked: model.setStatusText("Clicked");
    }

    StatusBar {
        anchors.bottom: parent.bottom;
        text: qsTr("Status: ") + model.statusText;
    }
}

My side projects

  • Online C++ code browser: code.woboq.org
  • Moc-ng: replacement of moc using libclang as a parser
  • verdigris: C++ header library with macro to do Qt without moc.
  • qmetaobject-rs: Rust bindings to Qt

Previous attempt at making Qt bindings for Rust

2014: cxx2rust: the pains of wrapping C++ in Rust on the example of Qt5

Conclusion: Automated bindings from rust to C++ is not working

  • Overloads
  • Default parameters
  • Lifetime (or absence thereof)
  • Nested enum and classes
  • Duplicated enum values
  • Templates
  • Macros

How to integrate C++ and Rust?

The Manual way


// In wrapper.cpp
extern "C" QLabel *showNewLabel(char *text)
{
    auto label = new QLabel(QString::fromUtf8(text));
    label->show();
    return label;
}
// ... and many more wrapping function

// in some rust module:
#[no_mangle]
extern fn showNewLabel(text : &CStr) -> *mut c_void;
// ...

bindgen


[build-dependencies]
bindgen = "0.20"

Automatically generates extern functions.

  • Only work on a small subset of C++.
  • Still not idiomatic rust

Rust Qt Binding Generator

Rust Qt Binding Generator


{
    "cppFile": "src/Bindings.cpp",
    "rust": {
        "dir": "rust",
        "interfaceModule": "interface",
        "implementationModule": "implementation"
    },
    "objects": {
        "Todos": {
            "type": "List",
            "properties": {
                "count": {
                    "type": "quint64"
                },
                "activeCount": {
                    "type": "quint64"
                }
            },
            "itemProperties": {
                "completed": {
                    "type": "bool",
                    "write": true
                },
                "description": {
                    "type": "QString",
                    "write": true
                }
            },
            "functions": {
                "add": {
                    "return": "void",
                    "mut": true,
                    "arguments": [{
		        "name": "description",
		        "type": "QString"
                    }]
                },
                "remove": {
                    "return": "bool",
                    "mut": true,
                    "arguments": [{
		        "name": "index",
		        "type": "quint64"
                    }]
                },
                "setAll": {
                    "return": "void",
                    "mut": true,
                    "arguments": [{
		        "name": "completed",
		        "type": "bool"
                    }]
                },
                "clearCompleted": {
                    "return": "void",
                    "mut": true,
                    "arguments": []
                }
            }
        }
    }
}

Rust Qt Binding Generator

  • You have to write a JSON file that will be parsed to create a Rust and C++ interface
  • You need to write some C++ code to call into the Qt API

rust_cpp


[package]
build = "build.rs"

[dependencies]
cpp = "0.4"

[build-dependencies]
cpp_build = "0.4"

The C++ code is directly embedded in the rust code


cpp!{{ #include<QLabel> }}

//...

unsafe {
 let text : &CStr = /* ... */;
 let label = cpp!([text as "const char*"] -> *mut c_void as "QLabel*" {
    auto label = new QLabel(QString::fromUtf8(text));
    label->show();
    return label;
 });
 // ...
}

build.rs


extern crate cpp_build;

fn main() {
    cpp_build::build("src/lib.rs");
}
  • Find all instances of cpp! and cpp_class! macro.
  • Generates extern "C" functions in a .cpp file.
  • Also generate meta-data array with information about size of types
  • Uses the 'cc' crate to compile the result (run the C++ compiler to produce a static lib)

cpp crate

  • maro_rules for cpp! and cpp_class! macro.
  • delegates the actual work to a custom derive macro implemented in cpp_macro

cpp_macro crate

  • Implementation detail with custom derives
  • Generate the call to the C++ function
  • Generate compile-time assert that the types have the same size/alignement

Example: exposing QImage

from the qmetaobject crate


cpp_class!(pub unsafe struct QImage as "QImage");
impl QImage {
    pub fn load_from_file(filename : QString) -> Self {
        cpp!(unsafe [filename as "QString"] -> QImage as "QImage" {
            return QImage(filename);
        })
    }
    pub fn new(size : QSize, format : ImageFormat) -> Self {
        cpp!(unsafe [size as "QSize", format as "QImage::Format" ] -> QImage as "QImage" {
            return QImage(size, format);
        })
    }
    pub fn size(&self) -> QSize {
        cpp!(unsafe [self as "const QImage*"] -> QSize as "QSize" { return self->size(); })
    }
    pub fn format(&self) -> ImageFormat {
        cpp!(unsafe [self as "const QImage*"] -> ImageFormat as "QImage::Format" { return self->format(); })
    }
    pub fn fill(&mut self, color: QColor) {
        cpp!(unsafe [self as "QImage*", color as "QColor"] { self->fill(color); })
    }
    pub fn set_pixel_color(&mut self, x:u32, y: u32, color: QColor) {
        cpp!(unsafe [self as "QImage*", x as "int", y as "int", color as "QColor"]
            { self->setPixelColor(x, y, color); })
    }
    pub fn get_pixel_color(&mut self, x:u32, y: u32) -> QColor {
        cpp!(unsafe [self as "QImage*", x as "int", y as "int"] -> QColor as "QColor"
            { return self->pixelColor(x, y); })
    }
    //...
}

rust!


cpp!{{
class Rust_QAbstractListModel : QAbstractListModel {
  TraitObject rust_object; // *mut dyn QAbstractListModel
public:
  QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
    return rust!(qalm_data [rust_object : &QAbstractListModel as "TraitObject",
                            index : QModelIndex as "QModelIndex", role : i32 as "int"]
            -> QVariant as "QVariant" {
        rust_object.data(index, role)
      });
    }

    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
        return rust!(qaml_setData [rust_object : &mut QAbstractListModel as "TraitObject",
                                   index : QModelIndex as "QModelIndex",
                                   value : QVariant as "QVariant",
                                   role : i32 as "int"] -> bool as "bool" {
            rust_object.set_data(index, &value, role)
        });
    }

    int rowCount(const QModelIndex & = QModelIndex()) const override {
        return rust!(qaml_rowCount [rust_object : &QAbstractListModel as "TraitObject"]
                -> i32 as "int" {
            rust_object.row_count()
        });
    }

    //...
};
}}

qmetaobject-rs


#[derive(QObject,Default)]
struct Greeter {
    base : qt_base_class!(trait QObject),
    name : qt_property!(QString; NOTIFY name_changed),
    name_changed : qt_signal!(),
    compute_greetings : qt_method!(fn compute_greetings(&self, verb : String) -> QString {
        return (verb + " " + &self.name.to_string()).into()
    })
}

qmetaobject-rs



fn main() {
    qml_register_type::<Greeter>(cstr!("Greeter"), 1, 0, cstr!("Greeter"));
    let mut engine = QmlEngine::new();
    engine.load_data(r#"import QtQuick 2.6;
import QtQuick.Window 2.0;
import Greeter 1.0
Window {
    visible: true;
    Greeter { id: greeter; name: 'World'; }
    Text { anchors.centerIn: parent; text: greeter.compute_greetings('hello'); }
}
"#.into());
    engine.exec();
}

Conclusion

Questions


Olivier Goffart

olivier@woboq.com

Twitter: @woboq

https://woboq.com

https://code.woboq.org