ndl@home

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

· updated · 7 comments

Announcement Essay

Discussion of Rust language features that make it difficult to generate & use C++ bindings in Rust.

Update: the discussion is happening at the following Reddit thread, please use it instead of comments here. Thanks!

Intro

I’ll summarize the issues I had with Rust while implementing cxx2rust (C++ bindings generator for Rust) using Qt5 as an example.

Note: no intent of flame wars / bashing - just pure constructive criticism :-)

Note2: the standard “I’m beginner in Rust” disclaimer apply, all corrections / suggestions are welcome!

Note3: all examples use rust-nightly-x86_64-unknown-linux-gnu from 16.05.2014.

cxx2rust

I have literally negative amount of free time now, so explanation of cxx2rust design and implementation details has to wait for separate (yet to be written) post.

Main points required to understand the text below:

Motivating examples

Let’s look at these two examples first.

Digital Clock

Here’s Digital Clock Qt5 demo translation to Rust that uses generated bindings:

// Import generated bindings.
extern crate Qt5Core;
extern crate Qt5Widgets;

use Qt5Core::{QObject, QString, QTime, QTimer};
use Qt5Widgets::{createApplication, QApplication, QLCDNumber, QLCDNumberExtra, QWidget};

// Similar to C++ version, but use composition instead of
// inheritance for QLCDNumber and also store QTimer
// explicitly to ensure it lives long enough.
struct DigitalClock<'a>
{
    lcd_number: QLCDNumber<'a>,
    timer: QTimer<'a>
}

impl<'a> DigitalClock<'a>
{
    pub fn new() -> DigitalClock
    {
        DigitalClock
        {
            lcd_number: QLCDNumber::new(&QWidget::null()),
            timer: QTimer::new(&QObject::null())
        }
    }

    pub fn init(&'a self)
    {
        self.lcd_number.setSegmentStyle(QLCDNumberExtra::Filled);
        self.lcd_number.setWindowTitle(&QString::new7("Digital Clock"));
        self.lcd_number.resize(150, 60);

        self.timer.timeout(self, |ref obj| { obj.showTime() } );
        self.timer.start(1000);

        self.showTime();
    }

    pub fn showTime(&self)
    {
        let time = QTime::currentTime();
        let text = time.toString2(&QString::new7("hh:mm"));

        if (time.second() % 2) == 0
        {
            // Use wrapped 'operator[]' and 'operator='
            // respectively.
            text.getByIndex2(2).assign2(' ' as i8);
        }
        self.lcd_number.display(&text);
    }

    pub fn show(&self)
    {
        self.lcd_number.show();
    }
}

fn main()
{
    let mut app = createApplication();
    // Take ownership of the returned object.
    app.owned = true;

    let clock = DigitalClock::new();
    clock.init();
    clock.show();

    QApplication::exec();
}

When compiled & executed, this shows the following nice window with blinking hours / minutes separator …

Digital Clock Screenshot

… and has the executable size of 68M :-) See discussion below for size issues.

Most of the code is either self-evident or direct translation of C++ code, so just several notes:

Image Viewer

Here’s Image Viewer Qt5 demo translation to Rust that uses generated bindings:

#![feature(globs)]

// Import generated bindings.
extern crate Qt5Core;
extern crate Qt5Gui;
extern crate Qt5Widgets;
extern crate Qt5PrintSupport;

// Lots of classes used, therefore import using globs.
use Qt5Core::*;
use Qt5Gui::*;
use Qt5Widgets::*;
use Qt5PrintSupport::*;

// Similar to C++ ImageViewer class, but use
// composition instead of inheritance for QMainWindow.
struct ImageViewer<'a>
{
    window: QMainWindow<'a>,

    imageLabel: QLabel<'a>,
    scrollArea: QScrollArea<'a>,

    openAct: QAction<'a>,
    printAct: QAction<'a>,
    exitAct: QAction<'a>,
    zoomInAct: QAction<'a>,
    zoomOutAct: QAction<'a>,
    normalSizeAct: QAction<'a>,
    fitToWindowAct: QAction<'a>,
    aboutAct: QAction<'a>,
    aboutQtAct: QAction<'a>,

    fileMenu: QMenu<'a>,
    viewMenu: QMenu<'a>,
    helpMenu: QMenu<'a>,

    printer: QPrinter<'a>,

    // Mutable data - wrap into RefCell
    scaleFactor: std::cell::RefCell<f64>
}

impl<'a> ImageViewer<'a>
{
    fn new() -> ImageViewer<'a>
    {
        let win_type_widget = QFlags_Qt_WindowType::new(Qt5Core::Qt::Widget);
        let win = QMainWindow::new(&QWidget::null(), &win_type_widget);
        let parent_obj = win.asQObject();
        let parent_widget = win.asQWidget();

        ImageViewer
        {
            window: win,

            imageLabel: QLabel::new(&QWidget::null(), &win_type_widget),
            scrollArea: QScrollArea::new(&QWidget::null()),

            openAct: ImageViewer::newAction("&Open...", Some("Ctrl+O"), &parent_obj),
            printAct: ImageViewer::newAction("&Print...", Some("Ctrl+P"), &parent_obj),
            exitAct: ImageViewer::newAction("E&xit", Some("Ctrl+Q"), &parent_obj),
            zoomInAct: ImageViewer::newAction("Zoom &In (25%)", Some("Ctrl++"), &parent_obj),
            zoomOutAct: ImageViewer::newAction("Zoom &Out (25%", Some("Ctrl+-"), &parent_obj),
            normalSizeAct: ImageViewer::newAction("&Normal Size", Some("Ctrl+S"), &parent_obj),
            fitToWindowAct: ImageViewer::newAction("&Fit to Window", Some("Ctrl+F"), &parent_obj),
            aboutAct: ImageViewer::newAction("&About", None, &parent_obj),
            aboutQtAct: ImageViewer::newAction("About &Qt", None, &parent_obj),

            fileMenu: QMenu::new2(&QString::new7("&File"), &parent_widget),
            viewMenu: QMenu::new2(&QString::new7("&View"), &parent_widget),
            helpMenu: QMenu::new2(&QString::new7("&Help"), &parent_widget),

            printer: QPrinter::new(QPrinterExtra::ScreenResolution),

            scaleFactor: std::cell::RefCell::new(1.0)
        }
    }

    // Utility function for easier actions construction. C++ version
    // doesn't have this, but we need it because of default arguments & non-trivial
    // strings construction.
    fn newAction(name: &str, shortcut: Option<&str>, parent: &QObject) -> QAction
    {
        let action = QAction::new2(&QString::new7(name), parent);
        match shortcut
        {
            Some(shortcut_str) =>
            {
                action.setShortcut(&QKeySequence::new2(&QString::new7(shortcut_str), QKeySequenceExtra::NativeText));
            }

            None => {}
        }

        action
    }

    // Split construction & initialization into separate phases due to lifetimes issues.
    fn init(&'a self)
    {
        self.imageLabel.setBackgroundRole(QPaletteExtra::Base);
        self.imageLabel.setSizePolicy2(QSizePolicyExtra::Ignored, QSizePolicyExtra::Ignored);
        self.imageLabel.setScaledContents(true);

        self.scrollArea.setBackgroundRole(QPaletteExtra::Dark);
        self.scrollArea.setWidget(&self.imageLabel.asQWidget());
        self.window.setCentralWidget(&self.scrollArea.asQWidget());

        self.openAct.triggered(self, |ref obj, _| { obj.open() });

        self.printAct.setEnabled(false);
        self.printAct.triggered(self, |ref obj, _| { obj.print() });

        // Note that closure argument is different, as we send close()
        // signal directly to the window object.
        self.exitAct.triggered(&self.window, |ref obj, _| { obj.close(); });

        self.zoomInAct.setEnabled(false);
        self.zoomInAct.triggered(self, |ref obj, _| { obj.zoomIn() });

        self.zoomOutAct.setEnabled(false);
        self.zoomOutAct.triggered(self, |ref obj, _| { obj.zoomOut() });

        self.normalSizeAct.setEnabled(false);
        self.normalSizeAct.triggered(self, |ref obj, _| { obj.normalSize() });

        self.fitToWindowAct.setEnabled(false);
        self.fitToWindowAct.setCheckable(true);
        self.fitToWindowAct.triggered(self, |ref obj, _| { obj.fitToWindow() });

        self.aboutAct.triggered(self, |ref obj, _| { obj.about() });

        // I don't need any closure arguments here, but I have to supply smth
        // because of the function signature, and generating two functions
        // (with and without argument) is likely not worth a hassle. Besides that,
        // due to the lack of overloading support I'd have to come up with
        // "creative" / ugly names for these anyway, which I'm not going to.
        self.aboutQtAct.triggered(self, |ref _obj, _| { QApplication::aboutQt() });

        self.fileMenu.addAction(&self.openAct);
        self.fileMenu.addAction(&self.printAct);
        self.fileMenu.addSeparator();
        self.fileMenu.addAction(&self.exitAct);

        self.viewMenu.addAction(&self.zoomInAct);
        self.viewMenu.addAction(&self.zoomOutAct);
        self.viewMenu.addAction(&self.normalSizeAct);
        self.viewMenu.addSeparator();
        self.viewMenu.addAction(&self.fitToWindowAct);

        self.helpMenu.addAction(&self.aboutAct);
        self.helpMenu.addAction(&self.aboutQtAct);

        self.window.menuBar().addMenu(&self.fileMenu);
        self.window.menuBar().addMenu(&self.viewMenu);
        self.window.menuBar().addMenu(&self.helpMenu);

        self.window.setWindowTitle(&QString::new7("Image Viewer"));
        self.window.resize(500, 400);
    }

    fn open(&self)
    {
        let fileName = QFileDialog::getOpenFileName(
            &self.window.asQWidget(),
            &QString::new7("Open File"),
            &QDir::currentPath(),
            &QString::new(),
            &QString::new(),
            &QFlags_QFileDialog_Option::new2(&QFlag::new(0))
        );

        if !fileName.isEmpty()
        {
            let image = QImage::new4(&fileName, "");
            if image.isNull()
            {
                QMessageBox::information(
                    &self.window.asQWidget(),
                    &QString::new7("Image Viewer"),
                    &QString::new7("Cannot load %1.").arg12(&fileName, 0, &QChar::new9(' ' as i8)),
                    &QFlags_QMessageBox_StandardButton::new(QMessageBoxExtra::Ok_OR_FirstButton),
                    QMessageBoxExtra::NoButton
                );
                return;
            }
            self.imageLabel.setPixmap(
                &QPixmap::fromImage(
                    &image,
                    &QFlags_Qt_ImageConversionFlag::new(
                        Qt5Core::Qt::AutoColor_OR_ThresholdAlphaDither_OR_DiffuseDither_OR_AutoDither
                    )
                )
            );
            self.setScaleFactor(1.0);

            self.printAct.setEnabled(true);
            self.fitToWindowAct.setEnabled(true);
            self.updateActions();

            if !self.fitToWindowAct.isChecked()
            {
                self.imageLabel.adjustSize();
            }
        }
    }

    fn print(&self)
    {
        let dialog = QPrintDialog::new(&self.printer, &self.window.asQWidget());
        if dialog.exec() != 0
        {
            let painter = QPainter::new2(&self.printer.asQPaintDevice());
            let rect = painter.viewport();
            let size = self.imageLabel.pixmap().size();
            size.scale2(&rect.size(), Qt5Core::Qt::KeepAspectRatio);
            painter.setViewport2(rect.x(), rect.y(), size.width(), size.height());
            painter.setWindow(&self.imageLabel.pixmap().rect());
            painter.drawPixmap9(0, 0, &self.imageLabel.pixmap());
        }
    }

    fn zoomIn(&self)
    {
        self.scaleImage(1.25);
    }

    fn zoomOut(&self)
    {
        self.scaleImage(0.8);
    }

    fn normalSize(&self)
    {
        self.imageLabel.adjustSize();
        self.setScaleFactor(1.0);
    }

    fn fitToWindow(&self)
    {
        let fitToWindow = self.fitToWindowAct.isChecked();
        self.scrollArea.setWidgetResizable(fitToWindow);
        if !fitToWindow
        {
            self.normalSize();
        }

        self.updateActions();
    }

    fn about(&self)
    {
        QMessageBox::about(
            &self.window.asQWidget(),
            &QString::new7("About Image Viewer"),
            &QString::new7(
                "<p>The <b>Image Viewer</b> example shows how to combine QLabel " +
                "and QScrollArea to display an image. QLabel is typically used " +
                "for displaying a text, but it can also display an image. " +
                "QScrollArea provides a scrolling view around another widget. " +
                "If the child widget exceeds the size of the frame, QScrollArea " +
                "automatically provides scroll bars. </p><p>The example " +
                "demonstrates how QLabel's ability to scale its contents " +
                "(QLabel::scaledContents), and QScrollArea's ability to " +
                "automatically resize its contents " +
                "(QScrollArea::widgetResizable), can be used to implement " +
                "zooming and scaling features. </p><p>In addition the example " +
                "shows how to use QPainter to print an image.</p>"
            )
        );
    }

    fn scaleImage(&self, factor: f64)
    {
        self.setScaleFactor(self.getScaleFactor() * factor);
        self.imageLabel.resize2(
            // Use the fact that size() returns temporary object copy
            // so we can safely modify it and use as an argument to resize().
            // multiplyAndAssign is automatic translation of 'operator*='.
            &self.imageLabel.pixmap().size().multiplyAndAssign(
                self.getScaleFactor()
            )
        );

        ImageViewer::adjustScrollBar(&self.scrollArea.horizontalScrollBar(), factor);
        ImageViewer::adjustScrollBar(&self.scrollArea.verticalScrollBar(), factor);

        self.zoomInAct.setEnabled(self.getScaleFactor() < 3.0);
        self.zoomOutAct.setEnabled(self.getScaleFactor() > 0.333);
    }

    fn adjustScrollBar(scrollBar: &QScrollBar, factor: f64)
    {
        scrollBar.setValue(
            (factor * scrollBar.value() as f64 +
             ((factor - 1.0) * scrollBar.pageStep() as f64 / 2.0)) as i32
        );
    }

    fn updateActions(&self)
    {
        self.zoomInAct.setEnabled(!self.fitToWindowAct.isChecked());
        self.zoomOutAct.setEnabled(!self.fitToWindowAct.isChecked());
        self.normalSizeAct.setEnabled(!self.fitToWindowAct.isChecked());
    }

    // Hide RefCell access mess.
    fn getScaleFactor(&self) -> f64
    {
        *self.scaleFactor.borrow()
    }

    // Hide RefCell access mess.
    fn setScaleFactor(&self, new_value: f64)
    {
        *self.scaleFactor.borrow_mut() = new_value;
    }

    fn show(&self)
    {
        self.window.show();
    }
}

fn main()
{
    let mut app = createApplication();
    // Take ownership of the returned object.
    app.owned = true;

    let viewer = ImageViewer::new();
    viewer.init();
    viewer.show();

    QApplication::exec();
}

Here’s an example of what it looks like: Image Viewer Screenshot

Issues analysis

Resulting binaries size

Digital Clock example compiles into 68M binary, Image Viewer - 70M. Given Qt5 libs are linked dynamically, these sizes are way too large.

Quick look using “nm” on the resulting executables shows huge amount of binding functions that are linked unnecessarily - that is, they’re used nor by the examples code, neither by the other bindings functions (as these are mostly independent of each other).

Nested enums and classes

In C++ it’s common to declare enums and helper structs / classes inside the class they relate to. This is apparently not possible in Rust. Moreover, because structs and mods share the same name scope - I cannot just declare sub-module with the same name as Qt class and define enums / utility classes there.

Therefore I have to move these nested items to their own name scopes. I’ve chosen “Extra” pattern, e.g. all enums defined inside QLCDNumber become the members of the module QLCDNumberExtra, which can be seen in Digital Clock example, line 30 or in many other places in Image Viewer demo (just search for “Extra”).

Duplicate enums values

C++ allows different enums elements to have the same value, here’s an example from Qt header:

template <typename T>
class QTypeInfo
{
public:
    enum {
        isPointer = false,
        isIntegral = QtPrivate::is_integral<T>::value,
        isComplex = true,
        isStatic = true,
        isLarge = (sizeof(T)>sizeof(void*)),
        isDummy = false,
        sizeOf = sizeof(T)
    };
};

This is not translatable directly to Rust enums, so I had to combine all enums with the same value into single item. One of the instantiations of the enum above, therefore, is translated like this:

pub mod QTypeInfo_QAbstractAnimation_ptrExtra {

  #[repr(C)]
  pub enum anon
  {
    isPointer = 1, isIntegral_OR_isComplex_OR_isStatic_OR_isLarge_OR_isDummy = 0, sizeOf = 8
  }

}

This is definitely not convenient and misses the whole purpose of this enum, but I see no other way of fixing it - modulo abandoning enums altogether and translating things to constants + int types, which crates its own share of problems like type safety.

Other examples of how ugly it gets can be seen in Image Viewer demo above, just search for “OR”.

Default parameters

Qt, as well as many other C++ libraries, heavily relies on default parameters to make API easier to use without sacrificing the functionality.

Because Rust doesn’t have default parameters, we need to explicitly specify them in all calls. Sometimes this has relatively small cost - like null QWidget parent in the case of Digital Clock example, line 23. However, in more complex scenarios it is really burdensome - see Image Viewer example, line 179: all extra arguments to arg12(), as well as all following arguments to QMessageBox::information are unnecessary and hinder readability.

Overloading

Another heavily used feature in C++ is overloading. Apparently Rust doesn’t support functions overloading either. Yes, I know it’s intentional and no, I strongly disagree with all philosophical reasons that try to “justify” it. I’m developing software in C++ starting from 90’s and while I’ve seen some rare examples of poor functions overloading use - this is still “must have” feature for any non-trivial framework, both from convenience and learning perspectives.

Anyway, arguments aside - the lack of overloading means there’s no reasonable way to use C++ methods names as Rust methods names without some form of deduplication. Because there’s no way I’m going to maintain the list of “intelligent” mappings for all Qt5 methods, the renaming is done automatically by adding a number after the function name - hence “QString::new7”, “toString2”, “arg12” etc.

There are 5 overloads like this in Digital Clock example and 26 (!) in Image Viewer.

Note this is completely non-obvious for bindings user (why not “new6” or “new8”?), require lengthy look-up in the generated bindings code for each such case - plus can easily change from version to version if overloads are added or removed, breaking code compilation.

However, other possibilities (like mangle the name using its signature) are arguably even worse.

Signals / slots

Here goes a long rant about the pains of slots implementation in Rust.

Given “closures” are mentioned as third most important feature right on the Rust language home page, I was sure that mapping Qt’s signals / slots to Rust’s closures would be a no-op. Boy was I wrong …

Apparently there are two types of closures. One type is tied to stack frame at the point of its definition. Another one can be accessed outside of its creation stack frame - but can be called only once.

Unfortunately, both types are unsuitable for signals / slots implementation. Stack-tied form is useless because in 99% cases you’ll want to set up the slot in some initialization function, whose stack frame will immediately expire. “Callable once” closures are useless because in 99% cases you want your slot to be called multiple times.

OK, screw it - there were no lambdas in C++’98 either, yet signals / slots were perfectly implementable using other language features. Can we do it the same way in Rust?

Surprisingly, the answer is “no”! Rust doesn’t even have member functions pointers! This means there’s no way to implement generic “callable” trait that would forward calls to the member function of your choice.

In short, from signals / slots perspective, the current set of language features in Rust is inferior to C++ of all versions starting from C++’98 (and yes, I’m not even talking about C++’11) - which to me was quite surprising, as hitting the mark “worse functional programming capabilities than in C++” is quite an “achievement” …

One possible “work-around” is to replace signals / slots with mere virtual functions-like polymorphism - that is, expect receiver to implement given trait & pass the reference to this trait to the signal.

This approach works fine for the simple example like Digital Clock, but would fail miserably for Image Viewer (or any other non-trivial code): one struct might want to implement different slots for the same signal signature, like “triggered()” mapping to “open()”, “print()”, “zoomIn()”, etc - depending which QAction triggered that signal. Using traits approach in this case would require implementing 9 different structs that either forward calls to the “main” ImageViewer struct or share enough state with ImageViewer to be able to perform these actions themselves. This is clear coding & maintenance nightmare (and the main reason why signals / slots decoupling pattern exists at all), so is not an option.

OK, so we cannot implement proper signals / slots in Rust with any built-in features, but it is supposed to have powerful macro system. Maybe we can implement some magic macros to get what we need?

Unfortunately, the answer is “no” again - at least not in any reasonable way. Rust macros system provides access to the AST too early - when semantic passes have not yet run, so there’s no type information available. In principle, we could likely get away even without type information (by making macro call slightly more verbose and inconvenient) but here’s another catch: current macro interface doesn’t seem to provide access to the whole AST tree - only to the tokens within a macro. Therefore I couldn’t find the way to look up the member function declaration from within the macro even if I’m willing to parse its declaration myself.

Failing to implement “proper” macro solution, one could revert to using external slots declarations produced during bindings generator run - but it already starts to look suspiciously similar to Qt’s MOC implementation (which BTW is not something I’m ever going to consider) plus when trying to implement this kind of macro I hit at least several non-obvious bugs both in macro system & reporting - at which point I was too frustrated with all this experience to continue further.

So, is all hope lost?

Apparently, there’s one small gap that we can use to our advantage. Stack-based closures require stack frame to be valid only in the case they’re capturing something. If we don’t capture anything - they can be stored & used outside of their creation stack frame.

On the first look, that does not buy us too much, as we do need access to captured variables to do anything useful - e.g. we need to capture “self” to call “self.zoomIn()”. However, this limited form of closures can be used to manually implement capturing - and then pass captured state as closure argument.

Here’s example that shows what I mean:

// Root trait for all callables.
pub trait CallableTrait { }

// 0 arguments implementation:
//
// ... skipped...
//

// 1 argument implementation:
pub type CallbackWithParam1<'a, Param, Ret, Arg0> = |&'a Param, Arg0|:'a -> Ret;

pub trait CallableTrait1<Ret, Arg0>: CallableTrait
{
    fn call(&self, arg0: Arg0) -> Ret;
}

pub trait CallableWithParamTrait1<'a, Param, Ret, Arg0>: CallableTrait1<Ret, Arg0> {}

pub struct CallableWithParam1<'a, Param, Ret, Arg0>
{
    pub param: &'a Param,
    pub callback: CallbackWithParam1<'a, Param, Ret, Arg0>
}

impl<'a, Param, Ret, Arg0> CallableTrait for CallableWithParam1<'a, Param, Ret, Arg0> {}

impl<'a, Param, Ret, Arg0> CallableTrait1<Ret, Arg0> for CallableWithParam1<'a, Param, Ret, Arg0>
{
    fn call(&self, arg0: Arg0) -> Ret
    {
        unsafe
        {
            // Rust doesn't allow to call closure by reference, but that's exactly
            // what we need to do here - hence the need for "unsafe" & pointer
            // magic.
            let self_uniq = &*self as *CallableWithParam1<'a, Param, Ret, Arg0>;
            ((*self_uniq).callback)(self.param, arg0)
        }
    }
}

impl<'a, Param, Ret, Arg0> CallableWithParamTrait1<'a, Param, Ret, Arg0> for CallableWithParam1<'a, Param, Ret, Arg0> {}

// 2 arguments implementation:
//
// ... skipped ...
//
// ... etc ...


struct Signal<'a>
{
    sig: std::cell::RefCell<Option<Box<CallableTrait>>>
}

impl<'a> Signal<'a>
{
    pub fn new() -> Signal
    {
        Signal { sig: std::cell::RefCell::new(None) }
    }

    pub fn set_slot<ObjType>(&self, obj: &'a ObjType, cb: CallbackWithParam1<'a, ObjType, i32, f64>)
    {
        // Work-around for overly strict borrow checker, having references inside boxed Callable1 is fine:
        // 1. References lifetime is 'a.
        // 2. Callable1 is assigned to 'sig'.
        // 3. 'sig' is a field of the 'Signal' struct with the same lifetime.
        // 4. 'sig' is not leaking its content outside of 'Signal'.
        // 5. Therefore, 'Callable1' is destroyed while reference is still valid.
        //
        // Casting is done in two stages:
        // 1. Cast to the sub-trait that propagates ObjType - necessary to silence compiler RE: 'static lifetime on ObjType.
        // 2. Unsafe transmuting into target trait.
        //
        // FIXME: I don't see any easier way to do this :-(
        let callable = box CallableWithParam1 { param: obj, callback: cb } as Box<CallableWithParamTrait1<'a, ObjType, i32, f64>>;
        unsafe
        {
            *self.sig.borrow_mut() = Some(std::mem::transmute(callable));
        }
    }

    pub fn trigger(&self, param0: f64) -> i32
    {
        unsafe
        {
            let r: &Box<CallableTrait1<i32, f64>> = std::mem::transmute(self.sig.borrow().get_ref());
            r.call(param0)
        }
    }
}

struct Test<'a>
{
    signal: Signal<'a>
}

impl<'a> Test<'a>
{
    pub fn new() -> Test
    {
        let t = Test { signal: Signal::new() };
        t.signal.set_slot(&t, |ref obj, param0| { obj.show(param0) });
        t
    }

    pub fn exec(&self)
    {
        println!("Signal call result: {}", self.signal.trigger(123.45))
    }

    pub fn show(&self, param0: f64) -> i32
    {
        println!("In slot call with argument {}", param0);
        return 15;
    }
}

fn main()
{
    let t = Test::new();
    t.exec();
}

What is happening here is that the state is stored in CallableWithParam1 and passed to the closure at the moment when signal is triggered - along with all the normal arguments signal provides. This process is fully transparent to the signal implementation - i.e. signal has no knowledge of the state being passed.

Of course this approach also has multiple limitations compared to the language-supported solution:

However, right now this seems to be the best one can do, so I’m using this approach to generate the bindings for signals and that’s what I’m using in both examples above.

Lifetimes

Regardless of signals / slots implementation issues described above, one has to be careful with lifetimes of the participating entities. Namely, we want to make sure the slot never expires before the signal, as that would result in dangling slot connection & eventual undefined behavior.

On the positive side, Rust shines here because lifetimes seem to be exactly what is needed - we should just make sure that slot reference lifetime is always as long as signals struct one.

On the negative side, there are several issues:

Summary

While I managed to generate the bindings & make these examples work - to me it feels that the set of issues outlined above would prevent any serious production use of these bindings. Therefore I really hope that at least the most pressing of these issues can be addressed in the Rust language design.

Please feel free to contact me with any questions / feedback. Corrections are appreciated as well :-)

7 comments

Huon · · #

Optimisations and closures

Are you compiling with optimisations (the -O or --opt-level flags)? Optimisations should remove most of the unused functions.

Also, the closures issue is known and is (progressively) being address eg. https://github.com/rust-lang/rfcs/pull/77 .

Also, unfortunately your closure hack is at risk of invoking undefined behaviour, so you need to be really really careful (specifically, mutating any data which is reachable by an & shared reference (not an &mut one) needs to be performed via a type like Cell or RefCell. This is the reason a closure can’t be called via &, because it could mutate a variable it captures).

Huon · · #

Optimisations and closures

Are you compiling with optimisations (the -O flag or --opt-level=<number>)? That should cause dead functions to be eliminated and so reduce the binary size.

Also, the problems with closures are known, and it’s being progressively improved (e.g. https://github.com/rust-lang/rfcs/pull/77 ).

By the way, your work-around has a very high risk of invoking undefined behaviour. The reason the compiler doesn’t let you call a closure through & is because all mutations of aliased data need to go via the Unsafe type (or one of the wrappers, like RefCell or Cell). Mutating via & without using Unsafe is undefined behaviour (in the sense of C/C++) and the compiler may “break” your program.

A closure can mutate its environment, hence calling one via & risks invoking undefined behaviour. However, it should be safe if you enforce that there are no captures (by giving the closure the 'static lifetime, e.g. |...|:'static -> ...).

It’s worth mentioning that this does mean that implementing

Things are getting even more complex if “mut” state for slots is allowed - up to the point I had to always use only non-mut and use RefCell<> for any mutable state that is accessed in slots.

requires special care.

The state is limited to only one variable. Multiple variables would complicate things even further by requiring to select the appropriate Callable struct implementation.

Rust has tuples (e.g. (int, f64)) so you can have a state being multiple variables by capturing a tuple.

Surprisingly, the answer is “no”! Rust doesn’t even have member functions pointers! This means there’s no way to implement generic “callable” trait that would forward calls to the member function of your choice.

A bare function pointer is writen fn(...) -> .... E.g.

struct Foo { f: fn(int) -> int }

fn increment(x: int) -> int { x + 1 }

fn main() {
    let foo = Foo { f: increment };

    println!("{}", (foo.f)(2));
}

There is some subtlety there, in that writing foo.f(2) is always interpreted as a method call, so the parentheses are required to disambiguate.

I didn’t manage to find the way to use lifetimes properly in “::new” functions. That is, in the examples above I have to construct the struct in two steps: calling “new” first and then “init” from the code that actually instantiates “DigitalClock” or “ImageViewer” structs, see lines 67 and 333 respectively.

At a guess, this is probably due to the &'a self annotation on init. This forces the 'a lifetime of the ImageViewer to be the stack frame of new, since that’s where the ImageViewer struct is placed when you call init (and hence the maximum time that the &self reference passed to init can last).

Theoretically the correct fix would be removing the 'a reference on the self reference, but I imagine that this would cause the captures of each slot to not last long enough. Another possible fix would be using a shared pointer equivalent (Rc) so that the captures do not need to have lifetimes like they have there… A third possible fix would be having some sort of action dispatcher on the ImageViewer type, which then passed a reference to itself into the action handler automatically (rather than “manually” capturing self for each one). However, I imagine this last one is very hard when using a library that’s not been designed for it.

(BTW, I’m having a lot of troubling getting past your captcha; it’s very hard even for a human!)

Huon · · #

Optimisations and closures

Are you compiling with optimisations (the -O flag or --opt-level=<number>)? That should cause dead functions to be eliminated and so reduce the binary size.

Also, the problems with closures are known, and it’s being progressively improved (e.g. https://github.com/rust-lang/rfcs/pull/77 ).

By the way, your work-around has a very high risk of invoking undefined behaviour. The reason the compiler doesn’t let you call a closure through & is because all mutations of aliased data need to go via the Unsafe type (or one of the wrappers, like RefCell or Cell). Mutating via & without using Unsafe is undefined behaviour (in the sense of C/C++) and the compiler may “break” your program.

A closure can mutate its environment, hence calling one via & risks invoking undefined behaviour. However, it should be safe if you enforce that there are no captures (by giving the closure the 'static lifetime, e.g. |...|:'static -> ...).

It’s worth mentioning that this does mean that implementing

Things are getting even more complex if “mut” state for slots is allowed - up to the point I had to always use only non-mut and use RefCell<> for any mutable state that is accessed in slots.

requires special care.

The state is limited to only one variable. Multiple variables would complicate things even further by requiring to select the appropriate Callable struct implementation.

Rust has tuples (e.g. (int, f64)) so you can have a state being multiple variables by capturing a tuple.

Surprisingly, the answer is “no”! Rust doesn’t even have member functions pointers! This means there’s no way to implement generic “callable” trait that would forward calls to the member function of your choice.

A bare function pointer is writen fn(...) -> .... E.g.

struct Foo { f: fn(int) -> int }

fn increment(x: int) -> int { x + 1 }

fn main() {
    let foo = Foo { f: increment };

    println!("{}", (foo.f)(2));
}

There is some subtlety there, in that writing foo.f(2) is always interpreted as a method call, so the parentheses are required to disambiguate.

I didn’t manage to find the way to use lifetimes properly in “::new” functions. That is, in the examples above I have to construct the struct in two steps: calling “new” first and then “init” from the code that actually instantiates “DigitalClock” or “ImageViewer” structs, see lines 67 and 333 respectively.

At a guess, this is probably due to the &'a self annotation on init. This forces the 'a lifetime of the ImageViewer to be the stack frame of new, since that’s where the ImageViewer struct is placed when you call init (and hence the maximum time that the &self reference passed to init can last).

Theoretically the correct fix would be removing the 'a reference on the self reference, but I imagine that this would cause the captures of each slot to not last long enough. Another possible fix would be using a shared pointer equivalent (Rc) so that the captures do not need to have lifetimes like they have there… A third possible fix would be having some sort of action dispatcher on the ImageViewer type, which then passed a reference to itself into the action handler automatically (rather than “manually” capturing self for each one). However, I imagine this last one is very hard when using a library that’s not been designed for it.

(BTW, I’m having a lot of troubling getting past your captcha; it’s very hard even for a human!)

Anonymous · · #

as hitting the mark “worse functional programming capabilities than in C++” is quite an “achievement” …

Yes, Rust is pretty much dead. It started out pretty nice but then the people behind it got carried away by some “beautiful type system” nonsense and forgot about pragmatism. Now you have a bunch of type system enthusiasts being paid for playing in a chocolate factory.

Gulshan · · #

Do you think things would go better with modern Rust? I’ve been wanting to use Qt in Rust for a while. Also, is the code for your bindings generator online somewhere?

Neel Basu · · #

Download and test cxx2rust

Is there any links ? from where I can download cxx2rust on some other libraries and templates

Neel Basu · · #

Download and test cxx2rust

Is there any links ? from where I can download cxx2rust on some other libraries and templates