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

Virtual destructor and custom wrapper data #164

Open
arnetheduck opened this issue Feb 5, 2025 · 2 comments
Open

Virtual destructor and custom wrapper data #164

arnetheduck opened this issue Feb 5, 2025 · 2 comments
Labels

Comments

@arnetheduck
Copy link
Contributor

In virtual classes, the destructor is a virtual function like any other that should be overrideable - this allows language projections to free resources when the object is deleted by third-party "owners" of the pointer, for example when it's part of a UI hierarchy where memory is managed by QT - a problem hinted at #17.

For this technique to be useful, the language projection also need to be able to place custom data in the derived class "somehow" (in c++, these would simply be additional members in the class).

No matter the style, all callbacks (virtual function overrides / destructor / signal handlers) would need access to this user data.

I'm contemplating 3 different models for this:

  1. "user" or "fat" pointers - here, the bindings pass in some arbitrary data that is passed back to the binding together with the qobject pointer.
void QObject_destructor_callback(QObject* self, void* userdata);

class MiqtVirtualQbject : public QObject {
  void* userdata;
  virtual MiqtVirtualObject~() { QObject_destructor_callback(this, userdata); }
};
QOjbect* QObject_new(..., void* userdata);
  1. vtable

This is a variation on the above theme where the "callback functions" are part of the data passed into the instance, potentially allowing different callback functions per "user-derived type". Per-instance "Custom data" for the binding can reside together with the VTable - a downside of this approach is that the vtable part often is static / known at compile time but per-instance data is not, so it's a little wasteful.

struct QObjectVTable  { 
  void (*destructor)(QObject* self, QObjectVTable* vtbl);
};
class MiqtVirtualQbject : public QObject {
  QObjectVTable* vtbl;
  virtual MiqtVirtualObject~() { vtbl->destructor(this, vtbl); }
};
QOjbect* QObject_new(..., OBjectVtable* userdata);
  1. Extra size

Instead of placing a pointer in the instance, one can simply pass in an "extra size" parameter that is used when allocating the derived class - this results in a memory layout similar to what inheriting from MiqtVirtualQbject would result in. It's a little bit more obscure as far as solutions go but has some ergonomic and performance advantages (single pointer, single allocation).

void QObject_destructor_callback(QObject* self, void* userdata);

// todo: alignment
void* QObject_userdata(MiqtVirtualQbject* p) { return static_cast<char*>(p)+sizeof(MiqtVirtualQbject); }

class MiqtVirtualQbject : public QObject {
    virtual MiqtVirtualObject~() { QObject_destructor_callback(this, userdata); }
};

QOjbect* QObject_new(..., size_t extradata, void** userdata) { 
  void* memory = operator new(sizeof(MiqtVirtualObject) + extrasize); 
  MiqtVirtualQbject* p = new(memory) ...;
  ...
  *userdata = QObject_userdata(p);
  return p;
}

n.b. QObject_userdata is an implementation detail not meant to be exposed in bindings - it would suffer the same dynamic_cast/panic problem as discussed here. Instead, we're careful to expose userdata only in contexts where the instance is guaranteed to hold the right type.

  1. const VTable + extra size

This is the "most flexible" approach and perhaps the one closest to what goes on behind the c++ scenes: the vtable is passed in as a pointer-to-const while extrasize is used for "user data" - this way, bindings can pre-allocate a set of vtables for each derived class and still place (mutable) data in each instance.

I'm leaning towards 3) or 4) for the nim bindings the technique is probably useful for all languages / bindings. 4) potentially has a little bit of extra flexibility but the const vtable is very similar to the "link-time callbacks" that miqt uses today and the "user data" already offers enough flexibility to handle this on the projection side - thoughts?

@mappu
Copy link
Owner

mappu commented Feb 6, 2025

Some earlier versions of miqt used the extended-allocation-size trick for miqt_string - it works well and saves a malloc.

A 5th trick for this is to connect to the QObject::destroyed signal, and do everything inside the language projection.

@mappu mappu added the wishlist label Feb 6, 2025
@arnetheduck
Copy link
Contributor Author

A 5th trick for this is to connect to the QObject::destroyed signal, and do everything inside the language projection.

I thought of this but it doesn't quite adhere to the natural C++ destructor order and thus breaks the "inheritance" illusion in subtle ways that I'd rather avoid.

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

No branches or pull requests

2 participants