Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subclassing API #26

Open
Riateche opened this issue Oct 3, 2016 · 8 comments
Open

Subclassing API #26

Riateche opened this issue Oct 3, 2016 · 8 comments

Comments

@Riateche
Copy link
Member

Riateche commented Oct 3, 2016

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
Copy link

o01eg commented Oct 27, 2017

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

@Riateche
Copy link
Member Author

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
Copy link

o01eg commented Oct 27, 2017

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

@snuk182
Copy link

snuk182 commented Jan 12, 2018

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
Copy link

melvyn2 commented Aug 30, 2022

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
Copy link

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
Copy link

melvyn2 commented Sep 12, 2022

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
Copy link

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. 👨🏽‍💻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants