You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Split a library into multiple modules cannot work properly on Windows (msys2/mingw) #355
The Qt library has multiple modules, such as QtCore, QtGui, etc. So I create a gem for each module, such as rubyqt6-qtcore, rubyqt6-qtgui, etc. (rubyqt6-qtgui depends on rubyqt6-core)
It works fine on Linux. But cause some runtime exceptions, like Type QObject is not defined ..., on Windows. Seems the DLL does not share global variables, and does not export any symbols. ref - difference between .so and .dll
The program load qtcore.so successfully, then load qtgui.so, but raises an exception.
I think, on windows, each DLL holds a copy of Rice::detail::Registries::instance. The Registries::instance in qtgui.so cannot getType of QObject which registered in qtcore.so.
First Try (failed)
Try to redefine class QObject on qtgui.so.
It loads successfully. But got Wrong argument type. Expected RubyQt6::QtCore::QObject. Recevied RubyQT6::QtCore::QObject exception when invoking QObject#meth (the method QObject#meth defined in the copy in qtcore.so).
Try to DLL export Rice::detail::Registries::instance in qtcore.so, and DLL import Rice::detail::Registries::instance in qtgui.so. I think this makes Registries::instance only one copy.
But it still doesn't work. Seems Data_Type<T>::check_is_bound() returns false because klass_ is nil. And it does not seem possible to DLL export Data_Type<T> because Rice automatically defines some raw types (e.g. Pointer<int>, Pointer<QObject>) when defining methods.
Any advice on how to get the rice to work with multiple modules on Windows ?
changed the title [-]Split a library into multiple modules cannot work properly on Windows[/-][+]Split a library into multiple modules cannot work properly on Windows (msys2/mingw)[/+]on Dec 3, 2025
That seems to me a good thing! The more likely scenario is two different extensions developed by two different authors that use Rice - you don't want them to step on each other. What if they both define the same class names? What if they use different versions of Rice? Etc.
I'm surprised it works on Linux. Are you sure? For example, each library should have its own Registries::instance. I wouldn't think the second library loaded would use the one from the first library, but that must be what's happening? But from your description, they are not sharing instantiated Data_Type<T> classes.
Anyway, I would say don't split up the extension into separate modules. I don't think its feasible. In fact (and sorry I know you won't like this) I think Rice should actively prevent this by compiling with -fvisibility=hidden on non-Windows OSes.
But let's say you really, really wanted to try this. You can work around the type registry issue by not throwing an error on unknown types.
Next you have to workaround Data_Type<T>::klass() issue. Each library is going to instantiate its own Data_Type<T> - that is unavoidable. I don't see a way to fix this in C++. But maybe via Ruby.
So let's use your example, Data_Type<QObject>. Assume it is setup in the first library via define_klass. But the second library will also instantiate the template Data_Type<QObject> - so it has its own copy. Of course, its static klass and ruby_data_type fields would be unset.
But maybe you could hack it by setting them manually. You would get the klass via Object.const_get (pass it the Ruby class name). From there, you could get the data_type via RTYPEDDATA(obj) and then grab the rb_data_type_t from the type field (well you would need an instance of the Ruby klass, so an object). Then set those static fields and in theory you would have a working Data_Type that is bound.
But you would have to hard-code that for every single class you are wrapping. You'd probably want a static `Data_Type::from_kalss(VALUE klass) method or some such. And the load order of the libraries becomes critical.
I think you are better off not doing this, but if you try, let me know if it works.
I'm surprised it works on Linux. Are you sure? For example, each library should have its own Registries::instance. I wouldn't think the second library loaded would use the one from the first library, but that must be what's happening? But from your description, they are not sharing instantiated Data_Type classes.
On Linux, GCC build c++ extension with following ldflags -rdynamic -Wl,-export-dynamic -Wl,--no-as-needed (the flags defined in RbConfig::CONFIG), so every library will export all symbols.
If I have understand right. Tho each library has its own Registries::instance, but they have same symbols, like _ZGVN4Rice6detail10Registries8instanceE, so the dynamic linker will only use the first found version, and all Data_Type<T> will be registered in the same instance.
Anyway, I would say don't split up the extension into separate modules. I don't think its feasible. In fact (and sorry I know you won't like this) I think Rice should actively prevent this by compiling with -fvisibility=hidden on non-Windows OSes.
The qt lib is a fairly big, and some modules are optional, such as qtwebkitcore, and some are only available on specific platforms, such as qtdbus. I also plan to add some unofficial libraries, such as kdelibs. So it is not possible to keep all the stuffs in one rubygem.
But let's say you really, really wanted to try this. You can work around the type registry issue by not throwing an error on unknown types.
...
Support for the Windows is a very low priority. I mainly use Linux. :-)
Now that I think about it, I could use an extern global tracker to trace all registered klasses and hack the Data_Type<T>::bind function to map all c++ Data_Type<QObject> defined in different libraries to the same ruby QObject klass ?
What if they use different versions of Rice? Etc.
When using bundler, the extensions will be compiled with the same Rice version.
That seems to me a good thing! The more likely scenario is two different extensions developed by two different authors that use Rice - you don't want them to step on each other. What if they both define the same class names?
Suppose you have two math libs, and they both define the same class, such as std::vector<int>, it will not be a problem on Linux in most cases, cuz the underlying c++ Data_Type<> is always refers to the first version.
But on Windows, the lib authors must use different ruby class name. The following code will cause an issue on Windows ():
It failed cuz Data_Type<std::vector<int>>() will invoke rb_define_class twice, and make ruby Std::Vector<Int> refers the later one. To fix this, we should use following code:
When using bundler, the extensions will be compiled with the same Rice version.
You can't make that assumption. You could have 2 different extension that use different versions of Rice (they have different versions specified in their gemspecs). You could then have a 3rd extension that simply copied the Rice headers into its source tree. So now you have 3 different versions of Rice in 3 different extensions. Bundler will do nothing to fix or avoid that.
Suppose you have two math libs, and they both define the same class, such as std::vector, it will not be a problem on Linux
Unless one was built against Rice 4.7 and the other one was built against 4.8 which added a new method to std::vector. If the extension that uses Rice 4.7 loads first, then you'll get an error in the second extension that uses 4.8 when it calls a non-existent method (although it would have been defined if it was loaded first).
I agree the shared classes (STL and API) on Windows are a problem due to the shared Std namespace. You could do what I wrote in a comment above to fix that (manually set klass and ruby_data_type on Data_Type<std::vector> or whatever) but that will be super brittle across Rice versions (what I wrote above).
I guess one way to avoid this is not have a standard Std namespace at all and have the extension writer specify a "default" namespace to add classes too it.
You can't make that assumption. You could have 2 different extension that use different versions of Rice (they have different versions specified in their gemspecs). You could then have a 3rd extension that simply copied the Rice headers into its source tree. So now you have 3 different versions of Rice in 3 different extensions. Bundler will do nothing to fix or avoid that.
...
I guess one way to avoid this is not have a standard Std namespace at all and have the extension writer specify a "default" namespace to add classes too it.
Yes. Allow developer using a custom namespace could resolve conflicts between two extensions. Maybe intro a RICE_NAMESPACE macro ?
Another approach is add a hidden attribute the Ruby Class, which would be the rb_data_type_t. Data_Type could then check for that in is defined.
But neither of these solutions solve the problem of libraries built with different versions of Rice (100% guaranteed to happen) or different versions of C++ library. That probably won't happen, but imagine one extension built with g++ and another with Clang. They have different C++ libraries underneath, which I assume means you cannot share things like std::vector across them.
Another approach is add a hidden attribute the Ruby Class, which would be the rb_data_type_t. Data_Type could then check for that in is defined.
So, you prefer to use klass to trace RbDataType instead of RubyQt6_RTypedData_Collector, and automatically bind it in the DataType::bind method ?
I will try it later.
But neither of these solutions solve the problem of libraries built with different versions of Rice (100% guaranteed to happen) or different versions of C++ library. That probably won't happen, but imagine one extension built with g++ and another with Clang. They have different C++ libraries underneath, which I assume means you cannot share things like std::vector across them.
Plan to add a RICE_NAMESPACE macro to solve this. E.g.
Ok, let's do this one step at a time. I agree with you that it probably makes most sense to have the ability to expose the type registry. By default it should be hidden and not accesible outside of an extension. However, in cases like yours we should make it possible to export it from one of the libraries and import it from the others.
I'd rather do this using a new Rice::Config class that is a constexpr and have various settings (export/import type registry, std ruby namespace, rice ruby namespace) then C macros. But I don't know if that is possible.
So 1 PR to add a Config class with the first setting controlling the visibility of the type registry. I believe you can just export the whole class versus having to do individual methods (although that would also work).
Once that works, then we can update Data_Type to search it when its enabled.
Ok, let's do this one step at a time. I agree with you that it probably makes most sense to have the ability to expose the type registry. By default it should be hidden and not accesible outside of an extension. However, in cases like yours we should make it possible to export it from one of the libraries and import it from the others.
Yes, in most cases, it should be ok.
But in my case, hiding the symbol in Linux will result to force using the method export it from one of the libraries and import it from the others. Since i can modify make_rice_headers.rb to generate a custom rice version, it will not be a big problem for me.
I'd rather do this using a new Rice::Config class that is a constexpr and have various settings (export/import type registry, std ruby namespace, rice ruby namespace) then C macros. But I don't know if that is possible.
So 1 PR to add a Config class with the first setting controlling the visibility of the type registry. I believe you can just export the whole class versus having to do individual methods (although that would also work).
Once that works, then we can update Data_Type to search it when its enabled.
I agree that using the Rice::Config class to set the rice/std ruby namespace is better than using C macros. Not sure if we need to export the entire class. It should only be DLL only, IMO.
In summary, we need to do the following things:
1. Fix exported symbol/name conflict between the extensions built against two different Rice version
If we don't want to use the C macro to implement this feature ( PR #360 ). I think we can implenent it through the following steps:
Add the cppflags -fvisibility=hidden or whatever to prevent the default export behavior on Linux.
Add the C++ class Rice::Config to allow developer to set the rice/std namespace for the current extension.
Update Module rb_mRice = define_moudule('rice') lines, using the name defined in Rice::Config
I don't have much experience with the C++ language. I'm not sure how to correctly set CPPFLAGS in Linux/MacOS, and use the constexpr. :-(
2. Provide a mechanism to allow Data_Type bind to (klass, ruby_data_type_t) defined in other DLL.
This feature allows us to split a library into multiple modules on Windows (Also on Linux built with -fvisibility=hidden cppflags). PR #358 just modifies Data_Type<T>::bind signature and expose it to public. With these small changes, the extension developers can import (klass, ruby_data_type_t) from other DLL, tho they still need to implement other stuffs.
If we want to implement the full mechanism in Rice lib, we need to:
Provide a way to trace all (klass, ruby_data_type_t).
Use RubyQt6_RTypedData_Collector, but this requires extra compiler options, e.g. RUBYQT6_BUILD_QTCORE_LIB.
Or add a hidden attribute to the Ruby class, not sure how to define the underlying type.
Update Data_Type<T>::bind or whatever, make it automatically register the ruby_data_type_t.
Update Data_Type<T>::is_defined or whatever, make it automatically create a new Data_Type<T> that bound to the global ruby_data_type_t
If this is not possible, the extension developers need to manually call declare_class_under after the DLL has been loaded.
Of course, we need a global flag defined in Rice::Config to turn on/off this behavoirs.
It will be much more developer-friendly than PR #358. Ideally, developers would only need to set a flag, to use this feature out-of-box. Tho it may not easy to implement, and make Rice lib a bit more complex.
I think that is a good summary of the needed steps - thanks for writing it up. I want to first get Rice 4.8 out the door, and get some changes upstreamed to Cmake. But I can hopefully give a try to #1 this weekend. Or if you want to give it a go I'm happy to review.
@souk4711 I haven't forgotten this. I first wanted to first get to a 4.8 release (done) and then publish the openv-ruby gem that I have been working on. Once that is done will come back to this.
@souk4711 I haven't forgotten this. I first wanted to first get to a 4.8 release (done) and then publish the openv-ruby gem that I have been working on. Once that is done will come back to this.
It's ok. Making rubyqt6 work on msys2 is optional. :-)
Activity
[-]Split a library into multiple modules cannot work properly on Windows[/-][+]Split a library into multiple modules cannot work properly on Windows (msys2/mingw)[/+]cfis commentedon Dec 4, 2025
That seems to me a good thing! The more likely scenario is two different extensions developed by two different authors that use Rice - you don't want them to step on each other. What if they both define the same class names? What if they use different versions of Rice? Etc.
I'm surprised it works on Linux. Are you sure? For example, each library should have its own
Registries::instance. I wouldn't think the second library loaded would use the one from the first library, but that must be what's happening? But from your description, they are not sharing instantiatedData_Type<T>classes.Anyway, I would say don't split up the extension into separate modules. I don't think its feasible. In fact (and sorry I know you won't like this) I think Rice should actively prevent this by compiling with
-fvisibility=hiddenon non-Windows OSes.But let's say you really, really wanted to try this. You can work around the type registry issue by not throwing an error on unknown types.
Next you have to workaround
Data_Type<T>::klass()issue. Each library is going to instantiate its ownData_Type<T>- that is unavoidable. I don't see a way to fix this in C++. But maybe via Ruby.So let's use your example,
Data_Type<QObject>. Assume it is setup in the first library viadefine_klass. But the second library will also instantiate the templateData_Type<QObject>- so it has its own copy. Of course, its staticklassandruby_data_typefields would be unset.But maybe you could hack it by setting them manually. You would get the klass via
Object.const_get(pass it the Ruby class name). From there, you could get the data_type viaRTYPEDDATA(obj)and then grab therb_data_type_tfrom thetypefield (well you would need an instance of the Ruby klass, so an object). Then set those static fields and in theory you would have a workingData_Typethat is bound.But you would have to hard-code that for every single class you are wrapping. You'd probably want a static `Data_Type::from_kalss(VALUE klass) method or some such. And the load order of the libraries becomes critical.
I think you are better off not doing this, but if you try, let me know if it works.
souk4711 commentedon Dec 4, 2025
On Linux, GCC build c++ extension with following ldflags
-rdynamic -Wl,-export-dynamic -Wl,--no-as-needed(the flags defined inRbConfig::CONFIG), so every library will export all symbols.If I have understand right. Tho each library has its own
Registries::instance, but they have same symbols, like_ZGVN4Rice6detail10Registries8instanceE, so the dynamic linker will only use the first found version, and allData_Type<T>will be registered in the same instance.The qt lib is a fairly big, and some modules are optional, such as
qtwebkitcore, and some are only available on specific platforms, such asqtdbus. I also plan to add some unofficial libraries, such askdelibs. So it is not possible to keep all the stuffs in onerubygem.Support for the Windows is a very low priority. I mainly use Linux. :-)
Now that I think about it, I could use an extern global tracker to trace all registered klasses and hack the
Data_Type<T>::bindfunction to map all c++Data_Type<QObject>defined in different libraries to the same rubyQObjectklass ?When using
bundler, the extensions will be compiled with the same Rice version.Suppose you have two math libs, and they both define the same class, such as
std::vector<int>, it will not be a problem on Linux in most cases, cuz the underlying c++Data_Type<>is always refers to the first version.But on Windows, the lib authors must use different ruby class name. The following code will cause an issue on Windows ():
It failed cuz
Data_Type<std::vector<int>>()will invokerb_define_classtwice, and make rubyStd::Vector<Int>refers the later one. To fix this, we should use following code:the
rice/api.hppalso not work properly.cfis commentedon Dec 5, 2025
You can't make that assumption. You could have 2 different extension that use different versions of Rice (they have different versions specified in their gemspecs). You could then have a 3rd extension that simply copied the Rice headers into its source tree. So now you have 3 different versions of Rice in 3 different extensions. Bundler will do nothing to fix or avoid that.
Unless one was built against Rice 4.7 and the other one was built against 4.8 which added a new method to std::vector. If the extension that uses Rice 4.7 loads first, then you'll get an error in the second extension that uses 4.8 when it calls a non-existent method (although it would have been defined if it was loaded first).
I agree the shared classes (STL and API) on Windows are a problem due to the shared
Stdnamespace. You could do what I wrote in a comment above to fix that (manually set klass and ruby_data_type on Data_Type<std::vector> or whatever) but that will be super brittle across Rice versions (what I wrote above).I guess one way to avoid this is not have a standard
Stdnamespace at all and have the extension writer specify a "default" namespace to add classes too it.souk4711 commentedon Dec 6, 2025
Yes. Allow developer using a custom namespace could resolve conflicts between two extensions. Maybe intro a
RICE_NAMESPACEmacro ?souk4711 commentedon Dec 6, 2025
1. Add
Data_Type::declare_class_underSee PR#358
2. Export a global variable
RubyQt6_RTypedData_Collector3. Trace
define_class_under, and addklass&RTypedDatatoRubyQt6_RTypedData_Collector4.
declare_class_underafter DLL loading ASAPNow, it should work without any
Type is not definedorType is not registeredexception. (Only tested on small samples.)cfis commentedon Dec 7, 2025
Another approach is add a hidden attribute the Ruby Class, which would be the
rb_data_type_t. Data_Type could then check for that in is defined.But neither of these solutions solve the problem of libraries built with different versions of Rice (100% guaranteed to happen) or different versions of C++ library. That probably won't happen, but imagine one extension built with g++ and another with Clang. They have different C++ libraries underneath, which I assume means you cannot share things like std::vector across them.
souk4711 commentedon Dec 7, 2025
So, you prefer to use
klassto traceRbDataTypeinstead ofRubyQt6_RTypedData_Collector, and automatically bind it in theDataType::bindmethod ?I will try it later.
Plan to add a
RICE_NAMESPACEmacro to solve this. E.g.cfis commentedon Dec 8, 2025
Ok, let's do this one step at a time. I agree with you that it probably makes most sense to have the ability to expose the type registry. By default it should be hidden and not accesible outside of an extension. However, in cases like yours we should make it possible to export it from one of the libraries and import it from the others.
I'd rather do this using a new Rice::Config class that is a constexpr and have various settings (export/import type registry, std ruby namespace, rice ruby namespace) then C macros. But I don't know if that is possible.
So 1 PR to add a Config class with the first setting controlling the visibility of the type registry. I believe you can just export the whole class versus having to do individual methods (although that would also work).
Once that works, then we can update Data_Type to search it when its enabled.
souk4711 commentedon Dec 9, 2025
Yes, in most cases, it should be ok.
But in my case, hiding the symbol in Linux will result to force using the method
export it from one of the libraries and import it from the others. Since i can modifymake_rice_headers.rbto generate a custom rice version, it will not be a big problem for me.I agree that using the
Rice::Configclass to set the rice/std ruby namespace is better than using C macros. Not sure if we need to export the entire class. It should only be DLL only, IMO.In summary, we need to do the following things:
1. Fix exported symbol/name conflict between the extensions built against two different Rice version
If we don't want to use the C macro to implement this feature ( PR #360 ). I think we can implenent it through the following steps:
-fvisibility=hiddenor whatever to prevent the default export behavior on Linux.Rice::Configto allow developer to set therice/stdnamespace for the current extension.Module rb_mRice = define_moudule('rice')lines, using the name defined inRice::ConfigI don't have much experience with the C++ language. I'm not sure how to correctly set CPPFLAGS in Linux/MacOS, and use the
constexpr. :-(2. Provide a mechanism to allow Data_Type bind to (klass, ruby_data_type_t) defined in other DLL.
This feature allows us to split a library into multiple modules on Windows (Also on Linux built with
-fvisibility=hiddencppflags). PR #358 just modifiesData_Type<T>::bindsignature and expose it topublic. With these small changes, the extension developers can import(klass, ruby_data_type_t)from other DLL, tho they still need to implement other stuffs.If we want to implement the full mechanism in Rice lib, we need to:
(klass, ruby_data_type_t).RubyQt6_RTypedData_Collector, but this requires extra compiler options, e.g.RUBYQT6_BUILD_QTCORE_LIB.Data_Type<T>::bindor whatever, make it automatically register theruby_data_type_t.Data_Type<T>::is_definedor whatever, make it automatically create a newData_Type<T>that bound to the globalruby_data_type_tdeclare_class_underafter the DLL has been loaded.Rice::Configto turn on/off this behavoirs.It will be much more developer-friendly than PR #358. Ideally, developers would only need to set a flag, to use this feature out-of-box. Tho it may not easy to implement, and make Rice lib a bit more complex.
cfis commentedon Dec 9, 2025
I think that is a good summary of the needed steps - thanks for writing it up. I want to first get Rice 4.8 out the door, and get some changes upstreamed to Cmake. But I can hopefully give a try to #1 this weekend. Or if you want to give it a go I'm happy to review.
cfis commentedon Dec 30, 2025
@souk4711 I haven't forgotten this. I first wanted to first get to a 4.8 release (done) and then publish the openv-ruby gem that I have been working on. Once that is done will come back to this.
souk4711 commentedon Dec 31, 2025
It's ok. Making rubyqt6 work on msys2 is optional. :-)