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

Tagged:

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:

  • cxx2rust parses C++ headers and generates Rust bindings for all classes, enums and global functions. This is done for template instantiations as well.
  • Classes are wrapped via heap (= by pointer to C++ object allocation), built-ins and enums - by value.
  • Supported functions arguments types have multiple limitations - but enough to wrap most Qt5 functions.

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:

  1. // Import generated bindings.
  2. extern crate Qt5Core;
  3. extern crate Qt5Widgets;
  4.  
  5. use Qt5Core::{QObject, QString, QTime, QTimer};
  6. use Qt5Widgets::{createApplication, QApplication, QLCDNumber, QLCDNumberExtra, QWidget};
  7.  
  8. // Similar to C++ version, but use composition instead of
  9. // inheritance for QLCDNumber and also store QTimer
  10. // explicitly to ensure it lives long enough.
  11. struct DigitalClock<'a>
  12. {
  13. lcd_number: QLCDNumber<'a>,
  14. timer: QTimer<'a>
  15. }
  16.  
  17. impl<'a> DigitalClock<'a>
  18. {
  19. pub fn new() -> DigitalClock
  20. {
  21. DigitalClock
  22. {
  23. lcd_number: QLCDNumber::new(&QWidget::null()),
  24. timer: QTimer::new(&QObject::null())
  25. }
  26. }
  27.  
  28. pub fn init(&'a self)
  29. {
  30. self.lcd_number.setSegmentStyle(QLCDNumberExtra::Filled);
  31. self.lcd_number.setWindowTitle(&QString::new7("Digital Clock"));
  32. self.lcd_number.resize(150, 60);
  33.  
  34. self.timer.timeout(self, |ref obj| { obj.showTime() } );
  35. self.timer.start(1000);
  36.  
  37. self.showTime();
  38. }
  39.  
  40. pub fn showTime(&self)
  41. {
  42. let time = QTime::currentTime();
  43. let text = time.toString2(&QString::new7("hh:mm"));
  44.  
  45. if (time.second() % 2) == 0
  46. {
  47. // Use wrapped 'operator[]' and 'operator='
  48. // respectively.
  49. text.getByIndex2(2).assign2(' ' as i8);
  50. }
  51. self.lcd_number.display(&text);
  52. }
  53.  
  54. pub fn show(&self)
  55. {
  56. self.lcd_number.show();
  57. }
  58. }
  59.  
  60. fn main()
  61. {
  62. let mut app = createApplication();
  63. // Take ownership of the returned object.
  64. app.owned = true;
  65.  
  66. let clock = DigitalClock::new();
  67. clock.init();
  68. clock.show();
  69.  
  70. QApplication::exec();
  71. }

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:

  • Qt5Widgets-based application depends on 3 Qt5 libs: QtCore, QtGui and QtWidgets. cxx2rust generates separate crates for each of these. However, because we’re using only classes from QtCore and QtWidgets, these are the only two we need to import.

  • QApplication construction is wrapped into function call at the C++ side. This is done to work-around “argc” and “argv” requirement its constructor has. Current C++ implementation just uses empty arguments.

  • Bindings generator has no idea whether Rust code should “own” returned objects (apart from the “new” calls, where it’s clear it should). Therefore, for all function calls other than “new”, it doesn’t take ownership at Rust side by default. Sometimes this assumption is wrong, like in the case of QApplication construction by external function (= not constructor), so I have to override this explicitly.

Image Viewer

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

  1. #![feature(globs)]
  2.  
  3. // Import generated bindings.
  4. extern crate Qt5Core;
  5. extern crate Qt5Gui;
  6. extern crate Qt5Widgets;
  7. extern crate Qt5PrintSupport;
  8.  
  9. // Lots of classes used, therefore import using globs.
  10. use Qt5Core::*;
  11. use Qt5Gui::*;
  12. use Qt5Widgets::*;
  13. use Qt5PrintSupport::*;
  14.  
  15. // Similar to C++ ImageViewer class, but use
  16. // composition instead of inheritance for QMainWindow.
  17. struct ImageViewer<'a>
  18. {
  19. window: QMainWindow<'a>,
  20.  
  21. imageLabel: QLabel<'a>,
  22. scrollArea: QScrollArea<'a>,
  23.  
  24. openAct: QAction<'a>,
  25. printAct: QAction<'a>,
  26. exitAct: QAction<'a>,
  27. zoomInAct: QAction<'a>,
  28. zoomOutAct: QAction<'a>,
  29. normalSizeAct: QAction<'a>,
  30. fitToWindowAct: QAction<'a>,
  31. aboutAct: QAction<'a>,
  32. aboutQtAct: QAction<'a>,
  33.  
  34. fileMenu: QMenu<'a>,
  35. viewMenu: QMenu<'a>,
  36. helpMenu: QMenu<'a>,
  37.  
  38. printer: QPrinter<'a>,
  39.  
  40. // Mutable data - wrap into RefCell
  41. scaleFactor: std::cell::RefCell<f64>
  42. }
  43.  
  44. impl<'a> ImageViewer<'a>
  45. {
  46. fn new() -> ImageViewer<'a>
  47. {
  48. let win_type_widget = QFlags_Qt_WindowType::new(Qt5Core::Qt::Widget);
  49. let win = QMainWindow::new(&QWidget::null(), &win_type_widget);
  50. let parent_obj = win.asQObject();
  51. let parent_widget = win.asQWidget();
  52.  
  53. ImageViewer
  54. {
  55. window: win,
  56.  
  57. imageLabel: QLabel::new(&QWidget::null(), &win_type_widget),
  58. scrollArea: QScrollArea::new(&QWidget::null()),
  59.  
  60. openAct: ImageViewer::newAction("&Open...", Some("Ctrl+O"), &parent_obj),
  61. printAct: ImageViewer::newAction("&Print...", Some("Ctrl+P"), &parent_obj),
  62. exitAct: ImageViewer::newAction("E&xit", Some("Ctrl+Q"), &parent_obj),
  63. zoomInAct: ImageViewer::newAction("Zoom &In (25%)", Some("Ctrl++"), &parent_obj),
  64. zoomOutAct: ImageViewer::newAction("Zoom &Out (25%", Some("Ctrl+-"), &parent_obj),
  65. normalSizeAct: ImageViewer::newAction("&Normal Size", Some("Ctrl+S"), &parent_obj),
  66. fitToWindowAct: ImageViewer::newAction("&Fit to Window", Some("Ctrl+F"), &parent_obj),
  67. aboutAct: ImageViewer::newAction("&About", None, &parent_obj),
  68. aboutQtAct: ImageViewer::newAction("About &Qt", None, &parent_obj),
  69.  
  70. fileMenu: QMenu::new2(&QString::new7("&File"), &parent_widget),
  71. viewMenu: QMenu::new2(&QString::new7("&View"), &parent_widget),
  72. helpMenu: QMenu::new2(&QString::new7("&Help"), &parent_widget),
  73.  
  74. printer: QPrinter::new(QPrinterExtra::ScreenResolution),
  75.  
  76. scaleFactor: std::cell::RefCell::new(1.0)
  77. }
  78. }
  79.  
  80. // Utility function for easier actions construction. C++ version
  81. // doesn't have this, but we need it because of default arguments & non-trivial
  82. // strings construction.
  83. fn newAction(name: &str, shortcut: Option<&str>, parent: &QObject) -> QAction
  84. {
  85. let action = QAction::new2(&QString::new7(name), parent);
  86. match shortcut
  87. {
  88. Some(shortcut_str) =>
  89. {
  90. action.setShortcut(&QKeySequence::new2(&QString::new7(shortcut_str), QKeySequenceExtra::NativeText));
  91. }
  92.  
  93. None => {}
  94. }
  95.  
  96. action
  97. }
  98.  
  99. // Split construction & initialization into separate phases due to lifetimes issues.
  100. fn init(&'a self)
  101. {
  102. self.imageLabel.setBackgroundRole(QPaletteExtra::Base);
  103. self.imageLabel.setSizePolicy2(QSizePolicyExtra::Ignored, QSizePolicyExtra::Ignored);
  104. self.imageLabel.setScaledContents(true);
  105.  
  106. self.scrollArea.setBackgroundRole(QPaletteExtra::Dark);
  107. self.scrollArea.setWidget(&self.imageLabel.asQWidget());
  108. self.window.setCentralWidget(&self.scrollArea.asQWidget());
  109.  
  110. self.openAct.triggered(self, |ref obj, _| { obj.open() });
  111.  
  112. self.printAct.setEnabled(false);
  113. self.printAct.triggered(self, |ref obj, _| { obj.print() });
  114.  
  115. // Note that closure argument is different, as we send close()
  116. // signal directly to the window object.
  117. self.exitAct.triggered(&self.window, |ref obj, _| { obj.close(); });
  118.  
  119. self.zoomInAct.setEnabled(false);
  120. self.zoomInAct.triggered(self, |ref obj, _| { obj.zoomIn() });
  121.  
  122. self.zoomOutAct.setEnabled(false);
  123. self.zoomOutAct.triggered(self, |ref obj, _| { obj.zoomOut() });
  124.  
  125. self.normalSizeAct.setEnabled(false);
  126. self.normalSizeAct.triggered(self, |ref obj, _| { obj.normalSize() });
  127.  
  128. self.fitToWindowAct.setEnabled(false);
  129. self.fitToWindowAct.setCheckable(true);
  130. self.fitToWindowAct.triggered(self, |ref obj, _| { obj.fitToWindow() });
  131.  
  132. self.aboutAct.triggered(self, |ref obj, _| { obj.about() });
  133.  
  134. // I don't need any closure arguments here, but I have to supply smth
  135. // because of the function signature, and generating two functions
  136. // (with and without argument) is likely not worth a hassle. Besides that,
  137. // due to the lack of overloading support I'd have to come up with
  138. // "creative" / ugly names for these anyway, which I'm not going to.
  139. self.aboutQtAct.triggered(self, |ref _obj, _| { QApplication::aboutQt() });
  140.  
  141. self.fileMenu.addAction(&self.openAct);
  142. self.fileMenu.addAction(&self.printAct);
  143. self.fileMenu.addSeparator();
  144. self.fileMenu.addAction(&self.exitAct);
  145.  
  146. self.viewMenu.addAction(&self.zoomInAct);
  147. self.viewMenu.addAction(&self.zoomOutAct);
  148. self.viewMenu.addAction(&self.normalSizeAct);
  149. self.viewMenu.addSeparator();
  150. self.viewMenu.addAction(&self.fitToWindowAct);
  151.  
  152. self.helpMenu.addAction(&self.aboutAct);
  153. self.helpMenu.addAction(&self.aboutQtAct);
  154.  
  155. self.window.menuBar().addMenu(&self.fileMenu);
  156. self.window.menuBar().addMenu(&self.viewMenu);
  157. self.window.menuBar().addMenu(&self.helpMenu);
  158.  
  159. self.window.setWindowTitle(&QString::new7("Image Viewer"));
  160. self.window.resize(500, 400);
  161. }
  162.  
  163. fn open(&self)
  164. {
  165. let fileName = QFileDialog::getOpenFileName(
  166. &self.window.asQWidget(),
  167. &QString::new7("Open File"),
  168. &QDir::currentPath(),
  169. &QString::new(),
  170. &QString::new(),
  171. &QFlags_QFileDialog_Option::new2(&QFlag::new(0))
  172. );
  173.  
  174. if !fileName.isEmpty()
  175. {
  176. let image = QImage::new4(&fileName, "");
  177. if image.isNull()
  178. {
  179. QMessageBox::information(
  180. &self.window.asQWidget(),
  181. &QString::new7("Image Viewer"),
  182. &QString::new7("Cannot load %1.").arg12(&fileName, 0, &QChar::new9(' ' as i8)),
  183. &QFlags_QMessageBox_StandardButton::new(QMessageBoxExtra::Ok_OR_FirstButton),
  184. QMessageBoxExtra::NoButton
  185. );
  186. return;
  187. }
  188. self.imageLabel.setPixmap(
  189. &QPixmap::fromImage(
  190. &image,
  191. &QFlags_Qt_ImageConversionFlag::new(
  192. Qt5Core::Qt::AutoColor_OR_ThresholdAlphaDither_OR_DiffuseDither_OR_AutoDither
  193. )
  194. )
  195. );
  196. self.setScaleFactor(1.0);
  197.  
  198. self.printAct.setEnabled(true);
  199. self.fitToWindowAct.setEnabled(true);
  200. self.updateActions();
  201.  
  202. if !self.fitToWindowAct.isChecked()
  203. {
  204. self.imageLabel.adjustSize();
  205. }
  206. }
  207. }
  208.  
  209. fn print(&self)
  210. {
  211. let dialog = QPrintDialog::new(&self.printer, &self.window.asQWidget());
  212. if dialog.exec() != 0
  213. {
  214. let painter = QPainter::new2(&self.printer.asQPaintDevice());
  215. let rect = painter.viewport();
  216. let size = self.imageLabel.pixmap().size();
  217. size.scale2(&rect.size(), Qt5Core::Qt::KeepAspectRatio);
  218. painter.setViewport2(rect.x(), rect.y(), size.width(), size.height());
  219. painter.setWindow(&self.imageLabel.pixmap().rect());
  220. painter.drawPixmap9(0, 0, &self.imageLabel.pixmap());
  221. }
  222. }
  223.  
  224. fn zoomIn(&self)
  225. {
  226. self.scaleImage(1.25);
  227. }
  228.  
  229. fn zoomOut(&self)
  230. {
  231. self.scaleImage(0.8);
  232. }
  233.  
  234. fn normalSize(&self)
  235. {
  236. self.imageLabel.adjustSize();
  237. self.setScaleFactor(1.0);
  238. }
  239.  
  240. fn fitToWindow(&self)
  241. {
  242. let fitToWindow = self.fitToWindowAct.isChecked();
  243. self.scrollArea.setWidgetResizable(fitToWindow);
  244. if !fitToWindow
  245. {
  246. self.normalSize();
  247. }
  248.  
  249. self.updateActions();
  250. }
  251.  
  252. fn about(&self)
  253. {
  254. QMessageBox::about(
  255. &self.window.asQWidget(),
  256. &QString::new7("About Image Viewer"),
  257. &QString::new7(
  258. "<p>The <b>Image Viewer</b> example shows how to combine QLabel " +
  259. "and QScrollArea to display an image. QLabel is typically used " +
  260. "for displaying a text, but it can also display an image. " +
  261. "QScrollArea provides a scrolling view around another widget. " +
  262. "If the child widget exceeds the size of the frame, QScrollArea " +
  263. "automatically provides scroll bars. </p><p>The example " +
  264. "demonstrates how QLabel's ability to scale its contents " +
  265. "(QLabel::scaledContents), and QScrollArea's ability to " +
  266. "automatically resize its contents " +
  267. "(QScrollArea::widgetResizable), can be used to implement " +
  268. "zooming and scaling features. </p><p>In addition the example " +
  269. "shows how to use QPainter to print an image.</p>"
  270. )
  271. );
  272. }
  273.  
  274. fn scaleImage(&self, factor: f64)
  275. {
  276. self.setScaleFactor(self.getScaleFactor() * factor);
  277. self.imageLabel.resize2(
  278. // Use the fact that size() returns temporary object copy
  279. // so we can safely modify it and use as an argument to resize().
  280. // multiplyAndAssign is automatic translation of 'operator*='.
  281. &self.imageLabel.pixmap().size().multiplyAndAssign(
  282. self.getScaleFactor()
  283. )
  284. );
  285.  
  286. ImageViewer::adjustScrollBar(&self.scrollArea.horizontalScrollBar(), factor);
  287. ImageViewer::adjustScrollBar(&self.scrollArea.verticalScrollBar(), factor);
  288.  
  289. self.zoomInAct.setEnabled(self.getScaleFactor() < 3.0);
  290. self.zoomOutAct.setEnabled(self.getScaleFactor() > 0.333);
  291. }
  292.  
  293. fn adjustScrollBar(scrollBar: &QScrollBar, factor: f64)
  294. {
  295. scrollBar.setValue(
  296. (factor * scrollBar.value() as f64 +
  297. ((factor - 1.0) * scrollBar.pageStep() as f64 / 2.0)) as i32
  298. );
  299. }
  300.  
  301. fn updateActions(&self)
  302. {
  303. self.zoomInAct.setEnabled(!self.fitToWindowAct.isChecked());
  304. self.zoomOutAct.setEnabled(!self.fitToWindowAct.isChecked());
  305. self.normalSizeAct.setEnabled(!self.fitToWindowAct.isChecked());
  306. }
  307.  
  308. // Hide RefCell access mess.
  309. fn getScaleFactor(&self) -> f64
  310. {
  311. *self.scaleFactor.borrow()
  312. }
  313.  
  314. // Hide RefCell access mess.
  315. fn setScaleFactor(&self, new_value: f64)
  316. {
  317. *self.scaleFactor.borrow_mut() = new_value;
  318. }
  319.  
  320. fn show(&self)
  321. {
  322. self.window.show();
  323. }
  324. }
  325.  
  326. fn main()
  327. {
  328. let mut app = createApplication();
  329. // Take ownership of the returned object.
  330. app.owned = true;
  331.  
  332. let viewer = ImageViewer::new();
  333. viewer.init();
  334. viewer.show();
  335.  
  336. QApplication::exec();
  337. }

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:

  1. // Root trait for all callables.
  2. pub trait CallableTrait { }
  3.  
  4. // 0 arguments implementation:
  5. //
  6. // ... skipped...
  7. //
  8.  
  9. // 1 argument implementation:
  10. pub type CallbackWithParam1<'a, Param, Ret, Arg0> = |&'a Param, Arg0|:'a -> Ret;
  11.  
  12. pub trait CallableTrait1<Ret, Arg0>: CallableTrait
  13. {
  14. fn call(&self, arg0: Arg0) -> Ret;
  15. }
  16.  
  17. pub trait CallableWithParamTrait1<'a, Param, Ret, Arg0>: CallableTrait1<Ret, Arg0> {}
  18.  
  19. pub struct CallableWithParam1<'a, Param, Ret, Arg0>
  20. {
  21. pub param: &'a Param,
  22. pub callback: CallbackWithParam1<'a, Param, Ret, Arg0>
  23. }
  24.  
  25. impl<'a, Param, Ret, Arg0> CallableTrait for CallableWithParam1<'a, Param, Ret, Arg0> {}
  26.  
  27. impl<'a, Param, Ret, Arg0> CallableTrait1<Ret, Arg0> for CallableWithParam1<'a, Param, Ret, Arg0>
  28. {
  29. fn call(&self, arg0: Arg0) -> Ret
  30. {
  31. unsafe
  32. {
  33. // Rust doesn't allow to call closure by reference, but that's exactly
  34. // what we need to do here - hence the need for "unsafe" & pointer
  35. // magic.
  36. let self_uniq = &*self as *CallableWithParam1<'a, Param, Ret, Arg0>;
  37. ((*self_uniq).callback)(self.param, arg0)
  38. }
  39. }
  40. }
  41.  
  42. impl<'a, Param, Ret, Arg0> CallableWithParamTrait1<'a, Param, Ret, Arg0> for CallableWithParam1<'a, Param, Ret, Arg0> {}
  43.  
  44. // 2 arguments implementation:
  45. //
  46. // ... skipped ...
  47. //
  48. // ... etc ...
  49.  
  50.  
  51. struct Signal<'a>
  52. {
  53. sig: std::cell::RefCell<Option<Box<CallableTrait>>>
  54. }
  55.  
  56. impl<'a> Signal<'a>
  57. {
  58. pub fn new() -> Signal
  59. {
  60. Signal { sig: std::cell::RefCell::new(None) }
  61. }
  62.  
  63. pub fn set_slot<ObjType>(&self, obj: &'a ObjType, cb: CallbackWithParam1<'a, ObjType, i32, f64>)
  64. {
  65. // Work-around for overly strict borrow checker, having references inside boxed Callable1 is fine:
  66. // 1. References lifetime is 'a.
  67. // 2. Callable1 is assigned to 'sig'.
  68. // 3. 'sig' is a field of the 'Signal' struct with the same lifetime.
  69. // 4. 'sig' is not leaking its content outside of 'Signal'.
  70. // 5. Therefore, 'Callable1' is destroyed while reference is still valid.
  71. //
  72. // Casting is done in two stages:
  73. // 1. Cast to the sub-trait that propagates ObjType - necessary to silence compiler RE: 'static lifetime on ObjType.
  74. // 2. Unsafe transmuting into target trait.
  75. //
  76. // FIXME: I don't see any easier way to do this :-(
  77. let callable = box CallableWithParam1 { param: obj, callback: cb } as Box<CallableWithParamTrait1<'a, ObjType, i32, f64>>;
  78. unsafe
  79. {
  80. *self.sig.borrow_mut() = Some(std::mem::transmute(callable));
  81. }
  82. }
  83.  
  84. pub fn trigger(&self, param0: f64) -> i32
  85. {
  86. unsafe
  87. {
  88. let r: &Box<CallableTrait1<i32, f64>> = std::mem::transmute(self.sig.borrow().get_ref());
  89. r.call(param0)
  90. }
  91. }
  92. }
  93.  
  94. struct Test<'a>
  95. {
  96. signal: Signal<'a>
  97. }
  98.  
  99. impl<'a> Test<'a>
  100. {
  101. pub fn new() -> Test
  102. {
  103. let t = Test { signal: Signal::new() };
  104. t.signal.set_slot(&t, |ref obj, param0| { obj.show(param0) });
  105. t
  106. }
  107.  
  108. pub fn exec(&self)
  109. {
  110. println!("Signal call result: {}", self.signal.trigger(123.45))
  111. }
  112.  
  113. pub fn show(&self, param0: f64) -> i32
  114. {
  115. println!("In slot call with argument {}", param0);
  116. return 15;
  117. }
  118. }
  119.  
  120. fn main()
  121. {
  122. let t = Test::new();
  123. t.exec();
  124. }

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:

  • The state has to be passed explicitly.
  • The state is limited to only one variable. Multiple variables would complicate things even further by requiring to select the appropriate Callable struct implementation.
  • All the gluing code is dependent on the number of arguments, so has to be written (or generated) for all numbers of arguments one needs.
  • There are 3 “unsafe” sections, this seems to be a lot for the code that essentially just tries to call the function.
  • 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.

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:

  • Specifying lifetimes statically might be overly restrictive: in principle, signals & slots can be connected & disconnected dynamically, so one can safely use even short-lived slot if it’s properly disconnected before its destruction. I don’t think Rust lifetime system is able to capture this behavior.

    This is not the major issue, though, as most of the time slots are not disconnected until both objects are destructed, plus it’s possible to implement automatic disconnect via Drop trait for the slot part.

  • Lifetime annotation is “infectious”: once Qt binding struct is annotated with it, all containing structs also have to be annotated, and that applies recursively. I wonder if having some hidden lifetime that corresponds to the containing struct lifetime would make things easier.

  • 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.

    Calling “init” directly from “new”, which would be the most logical choice, doesn’t work because Rust doesn’t understand the object returned by “new” has the same lifetime as the whole struct - and I failed to find the way to convince the compiler this is the case. I really hope I’m missing something here, as this must be pretty common case and if it’s not possible to express it in the language - that is a real problem.

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

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

> as hitting the mark “worse

> 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.

Optimisations and closures

Are you compiling with optimisations (the -O flag or --opt-level=)? 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!)

Post new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.
Syndicate content

User login