rust-qt / ritual

Use C++ libraries from Rust
Apache License 2.0
1.22k stars 49 forks source link

Subclassing API #26

Open Riateche opened 7 years ago

Riateche commented 7 years ago

Let's have a C++ class that can be subclassed:

class Example {
public:
  Example(int x);
  virtual ~Example();
  virtual int vfunc2(double y);
protected:
  void func1();
};

First, we create a C++ wrapper:

class ExampleSubclass : public Example {
  ExampleSubclass(int x);
  ~ExampleSubclass();
  void func1();
  int vfunc2(double y);

  void set_vfunc2_func(int (*func)(void*, double), void* data);
  void set_destructor_func(void (*func)(void*), void* data);
private:
  int (*m_func2_func)(void*, double);
  void* m_func2_data;

  void (*m_destructor_func)(void*);
  void* m_destructor_data;

};

ExampleSubclass::ExampleSubclass(int x) : Example(x) {
  m_func2_func = 0;
  m_func2_data = 0;
  m_destructor_func = 0;
  m_destructor_data = 0;
}

ExampleSubclass::~ExampleSubclass() {
  if (m_destructor_func) {
    m_destructor_func(m_destructor_data);
  }
}

void ExampleSubclass::func1() {
  return Example::func1();
}

void ExampleSubclass::set_vfunc2_func(int (*func)(void*, double), void* data) {
  m_func2_func = func;
  m_func2_data = data;
}

void ExampleSubclass::set_destructor_func(void (*func)(void*), void* data) {
  m_destructor_func = func;
  m_destructor_data = data;
}

int ExampleSubclass::vfunc2(double y) {
  if (m_func2_func) {
    return m_func2_func(m_func2_data, y);
  } else {
    return Example::vfunc2(y);
    // or abort() if the method was pure virtual
  }
}

The wrapper exposes all protected functions of the base class and reimplements all its virtual functions. It allows to add callbacks for each virtual method. If a callback is not set, it calls base class implementation, as if the method was not reimplemented. It also allows to add a destructor callback for cleanup purposes.

Rust API changes the order of operations. First, the user needs to assign virtual functions. Then the object can be created:

let x = ExampleSubclassBuilder::new();
x.bind_func2(|arg| arg as i32);
let object: CppBox<ExampleSubclass> = x.new(constructor_arg);

If not all pure virtual functions were bound, ExampleSubclassBuilder::new function will panic.

ExampleSubclassBuilder::new creates a Rust struct that owns all lambdas and ensures that they are not deleted until the object itself is deleted. The struct is kept in memory by calling mem::forget. The destructor callback is used to delete this struct when it's not needed anymore. If the base class's destructor is virtual, you can pass the ExampleSubclass object's ownership to the C++ library. When it delets the object, the Rust struct cleanup will still be done. If the base class's destructor is not virtual, the only correct way to release the resources is to let CppBox delete the object.

ExampleSubclass Rust type is just a wrapper for ExampleSubclass C++ class, similar to other class wrappers. It exposes all its public methods, thus providing access to protected methods of the base class. Callback setters are not exposed. ExampleSubclass will provide means to downcast it to Example base type, just as any other derived class.

Example of initialization of a Rust struct with a subclass field:

impl MyExampleSubclass {
  pub fn new(arg: i32) -> MyExampleSubclass {
    let mut obj = MyExampleSubclass {
      example: CppBox::null(),
      other_data: 42,
    };
    let subclass = ExampleSubclassBuilder::new();
    x.bind_func2(|arg| {
      // can capture `self` or its field in some form here
      arg as i32
    });
    obj.example = x.new(arg);
    obj
  }
}
o01eg commented 6 years ago

Could it possible use https://github.com/rust-lang-nursery/rust-bindgen way?

Riateche commented 6 years ago

Can you describe what is the way you mean? I found the doc page that says that they don't support cross language inheritance at all.

o01eg commented 6 years ago

They support inheritance (its the first stated) and even somehow work with vtable as I've seen in their code.

snuk182 commented 6 years ago

Meanwhile I have created a QObject wrapper which holds the Rust closure for custom event filtering. Maybe it can be useful. https://github.com/snuk182/qt_core_custom_events

melvyn2 commented 1 year ago

That crate is exactly what I needed... but is outdated and broken. Is there any way to create event filter QObjects using just the base ritual Qt crates?

Update: I was using the crate wrong and the event filters do work. Still a bit of a hassle, but at least theres a solution.

aryaminus commented 1 year ago

That crate is exactly what I needed... but is outdated and broken. Is there any way to create event filter QObjects using just the base ritual Qt crates?

Update: I was using the crate wrong and the event filters do work. Still a bit of a hassle, but at least theres a solution.

Hi Melvyn. Do you mind sharing a snippet of how you used it? Having a hard time making it work

melvyn2 commented 1 year ago

Sure, it's pretty messy (first attempts) but works:

unsafe fn add_event_filters(self: &Rc<Self>) {
    fn file_list_event_filter(obj: &mut QObject, event: &mut QEvent) -> bool {
        // Function body has to be unsafe rather than function, because the filter requires an FnMut
        // Which only safe function pointers are
        unsafe {
            if event.type_() == q_event::Type::DragEnter {
                println!("Trace: received DragEnter event.");
                // Transmute is safe because we check the event type
                let devent: &mut QDragEnterEvent = transmute(event);
                let mime_data = devent.mime_data().text().to_std_string();
                // There is a method QMimeData.urls() but dealing with QLists is no fun
                let urls: Vec<&str> = mime_data.lines().collect();
                // Check if there are any files, excluding paths ending with / (dirs)
                if devent.mime_data().has_urls()
                    && urls
                        .iter()
                        .any(|url| !url.is_empty() && !url.ends_with('/'))
                {
                    println!("Trace: event has valid data, accepting.");
                    devent.set_drop_action(DropAction::LinkAction);
                    // If we don't accept the DragEnter event, the DropEvent won't trigger
                    devent.accept();
                    return true;
                }
            } else if event.type_() == q_event::Type::Drop {
                println!("Trace: received Drop event.");
                // Transmute is safe because we check the event type
                let devent: &mut QDropEvent = transmute(event);

                let obj_type = CStr::from_ptr(obj.meta_object().class_name());
                if obj_type.to_bytes() != b"QListWidget" {
                    println!(
                        "Error: received even on wrong QObject ({:?} instead of QWidget)",
                        obj_type
                    );
                    return false;
                }
                // Transmute is safe because we check the widget type
                let list_widget: &mut QListWidget = transmute(obj);

                let mime_data = devent.mime_data().text().to_std_string();
                let urls: Vec<&str> = mime_data.lines().collect();
                for file in urls.iter().filter(|f| !f.ends_with('/')) {
                    list_widget.add_item_q_string(qs(file.replacen("file://", "", 1)).as_ref());
                }
                devent.set_drop_action(DropAction::LinkAction);
                devent.accept();
                return true;
            }
            return false;
        }
    }
    self.ui.lib_list.install_event_filter(
        CustomEventFilter::new(file_list_event_filter).into_raw_ptr(),
    );
}
aryaminus commented 1 year ago

Sure, it's pretty messy (first attempts) but works:

unsafe fn add_event_filters(self: &Rc<Self>) {
    fn file_list_event_filter(obj: &mut QObject, event: &mut QEvent) -> bool {
        // Function body has to be unsafe rather than function, because the filter requires an FnMut
        // Which only safe function pointers are
        unsafe {
            if event.type_() == q_event::Type::DragEnter {
                println!("Trace: received DragEnter event.");
                // Transmute is safe because we check the event type
                let devent: &mut QDragEnterEvent = transmute(event);
                let mime_data = devent.mime_data().text().to_std_string();
                // There is a method QMimeData.urls() but dealing with QLists is no fun
                let urls: Vec<&str> = mime_data.lines().collect();
                // Check if there are any files, excluding paths ending with / (dirs)
                if devent.mime_data().has_urls()
                    && urls
                        .iter()
                        .any(|url| !url.is_empty() && !url.ends_with('/'))
                {
                    println!("Trace: event has valid data, accepting.");
                    devent.set_drop_action(DropAction::LinkAction);
                    // If we don't accept the DragEnter event, the DropEvent won't trigger
                    devent.accept();
                    return true;
                }
            } else if event.type_() == q_event::Type::Drop {
                println!("Trace: received Drop event.");
                // Transmute is safe because we check the event type
                let devent: &mut QDropEvent = transmute(event);

                let obj_type = CStr::from_ptr(obj.meta_object().class_name());
                if obj_type.to_bytes() != b"QListWidget" {
                    println!(
                        "Error: received even on wrong QObject ({:?} instead of QWidget)",
                        obj_type
                    );
                    return false;
                }
                // Transmute is safe because we check the widget type
                let list_widget: &mut QListWidget = transmute(obj);

                let mime_data = devent.mime_data().text().to_std_string();
                let urls: Vec<&str> = mime_data.lines().collect();
                for file in urls.iter().filter(|f| !f.ends_with('/')) {
                    list_widget.add_item_q_string(qs(file.replacen("file://", "", 1)).as_ref());
                }
                devent.set_drop_action(DropAction::LinkAction);
                devent.accept();
                return true;
            }
            return false;
        }
    }
    self.ui.lib_list.install_event_filter(
        CustomEventFilter::new(Self::file_list_event_filter).into_raw_ptr(),
    );
}

Thanks Melvyn. Had been experimenting with Paint events, still maneuvering around to make it work. 👨🏽‍💻