From 7ee008e150bc5bcf76082d726f719ee0fdfda982 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 11 Feb 2026 02:37:39 -0600 Subject: Fluorine Manager: full Linux port of Mod Organizer 2 Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 --- .../src/pybind11-utils/CMakeLists.txt | 23 +++ libs/plugin_python/src/pybind11-utils/README.md | 50 ++++++ .../src/pybind11-utils/functional.cpp | 28 ++++ .../include/pybind11_utils/functional.h | 155 ++++++++++++++++++ .../include/pybind11_utils/generator.h | 57 +++++++ .../include/pybind11_utils/shared_cpp_owner.h | 90 +++++++++++ .../include/pybind11_utils/smart_variant.h | 53 ++++++ .../include/pybind11_utils/smart_variant_wrapper.h | 180 +++++++++++++++++++++ 8 files changed, 636 insertions(+) create mode 100644 libs/plugin_python/src/pybind11-utils/CMakeLists.txt create mode 100644 libs/plugin_python/src/pybind11-utils/README.md create mode 100644 libs/plugin_python/src/pybind11-utils/functional.cpp create mode 100644 libs/plugin_python/src/pybind11-utils/include/pybind11_utils/functional.h create mode 100644 libs/plugin_python/src/pybind11-utils/include/pybind11_utils/generator.h create mode 100644 libs/plugin_python/src/pybind11-utils/include/pybind11_utils/shared_cpp_owner.h create mode 100644 libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant.h create mode 100644 libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant_wrapper.h (limited to 'libs/plugin_python/src/pybind11-utils') diff --git a/libs/plugin_python/src/pybind11-utils/CMakeLists.txt b/libs/plugin_python/src/pybind11-utils/CMakeLists.txt new file mode 100644 index 0000000..77895b6 --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(pybind11-utils STATIC + ./include/pybind11_utils/functional.h + ./include/pybind11_utils/generator.h + ./include/pybind11_utils/shared_cpp_owner.h + ./include/pybind11_utils/smart_variant_wrapper.h + ./include/pybind11_utils/smart_variant.h + + functional.cpp +) +mo2_configure_target(pybind11-utils + NO_SOURCES + WARNINGS 4 + EXTERNAL_WARNINGS 4 + AUTOMOC OFF + TRANSLATIONS OFF +) +mo2_default_source_group() +target_link_libraries(pybind11-utils PUBLIC pybind11::pybind11) +target_include_directories(pybind11-utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) + +add_library(pybind11::utils ALIAS pybind11-utils) diff --git a/libs/plugin_python/src/pybind11-utils/README.md b/libs/plugin_python/src/pybind11-utils/README.md new file mode 100644 index 0000000..46a9256 --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/README.md @@ -0,0 +1,50 @@ +# pybind11-utils + +This library contains some utility stuff for `pybind11` + +## smart_variant_wrapper.h + +Expose a function `mo2::python::wrap_arguments` and a template +`mo2::python::smart_variant` that can be used to expose more interesting types Python +than the C++ one, e.g., accept `os.PathLike` and `QFileInfo` when a simple `QString` is +expected. + +A toy example can be found in the test folder at +[`tests/python/test_argument_wrapper.cpp`](../../tests/python/test_argument_wrapper.cpp). + +More concrete examples can be found in +[`mobase/pybind11_all.h`](../mobase/pybind11_all.h) for `FileWrapper` and +`DirectoryWrapper`. + +## functional.h + +TODO: updated version of `` that should check the signature +a bit more when creating `std::function` (similar to previous implementation). + +## shared_cpp_owner.h + +Expose a macro `MO2_PYBIND11_SHARED_CPP_HOLDER` that can be used to declare that +`std::shared_ptr<...>` must hold-on their associate Python object. + +```cpp +// use the macro on the type to be exposed (with a trampoline class) +MO2_PYBIND11_SHARED_CPP_HOLDER(ISaveGame) + +// use std::shared_ptr<> as the holder for the class +py::class_>(...); +``` + +Using the `MO2_PYBIND11_SHARED_CPP_HOLDER` (must be present in all files manipulating +`ISaveGame` between C++ and Python) ensure that the Python instance remains alive +alongside the C++ one. + +The `MO2_PYBIND11_SHARED_CPP_HOLDER` declares a specialization of `type_caster<>` that +alters the `std::shared_ptr` by return a `std::shared_ptr<>` that owns the Python +object (via a `pybind11::object`) but does not release the C++ one - The C++ object is +owned by the Python one, so the relation is as follows: + +- The `std::shared_ptr` manipulated in C++ maintains the `pybind11::object` alive + through a custom deleter but DOES NOT release the C++ object when the reference count + reaches 0. +- The Python object holds a standard `std::shared_ptr` that will release the object + when the reference count reaches 0. diff --git a/libs/plugin_python/src/pybind11-utils/functional.cpp b/libs/plugin_python/src/pybind11-utils/functional.cpp new file mode 100644 index 0000000..4cc7b6c --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/functional.cpp @@ -0,0 +1,28 @@ +#include "pybind11_utils/functional.h" + +namespace py = pybind11; + +namespace mo2::python::detail { + + bool has_compatible_arity(py::function fn, std::size_t arity) + { + auto inspect = py::module_::import("inspect"); + auto arg_spec = inspect.attr("getfullargspec")(fn); + py::object args = arg_spec.attr("args"), varargs = arg_spec.attr("varargs"), + defaults = arg_spec.attr("defaults"); + + auto args_count = args.is(py::none()) ? 0 : py::len(args); + auto defaults_count = defaults.is(py::none()) ? 0 : py::len(defaults); + + if (inspect.attr("ismethod")(fn).cast() && py::hasattr(fn, "__self__")) { + --args_count; + } + + auto required_count = args_count - defaults_count; + + return required_count <= arity // cannot require more parameters than given, + && (args_count >= arity || + !varargs.is_none()); // must accept enough parameters. + } + +} // namespace mo2::python::detail diff --git a/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/functional.h b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/functional.h new file mode 100644 index 0000000..8662d82 --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/functional.h @@ -0,0 +1,155 @@ +#ifndef PYTHON_PYBIND11_FUNCTIONAL_H +#define PYTHON_PYBIND11_FUNCTIONAL_H + +#include + +namespace mo2::python::detail { + + // check if the given function is valid for a C++ function with the + // given arity + // + bool has_compatible_arity(pybind11::function handle, std::size_t arity); + +} // namespace mo2::python::detail + +namespace pybind11::detail { + + // custom type_caster for std::function<> + // + // most of this is from pybind11 except that we also check arity of the function to + // allow overloaded function based on arity of argument + // + template + struct type_caster> { + using type = std::function; + using retval_type = + conditional_t::value, void_type, Return>; + using function_type = Return (*)(Args...); + + public: + bool load(handle src, bool convert) + { + if (src.is_none()) { + // Defer accepting None to other overloads (if we aren't in convert + // mode): + if (!convert) { + return false; + } + return true; + } + + if (!isinstance(src)) { + return false; + } + + auto func = reinterpret_borrow(src); + + /* + When passing a C++ function as an argument to another C++ + function via Python, every function call would normally involve + a full C++ -> Python -> C++ roundtrip, which can be prohibitive. + Here, we try to at least detect the case where the function is + stateless (i.e. function pointer or lambda function without + captured variables), in which case the roundtrip can be avoided. + */ + if (auto cfunc = func.cpp_function()) { + auto* cfunc_self = PyCFunction_GET_SELF(cfunc.ptr()); + if (isinstance(cfunc_self)) { + auto c = reinterpret_borrow(cfunc_self); + auto* rec = (function_record*)c; + + while (rec != nullptr) { + if (rec->is_stateless && + same_type(typeid(function_type), + *reinterpret_cast( + rec->data[1]))) { + struct capture { + function_type f; + }; + value = ((capture*)&rec->data)->f; + return true; + } + rec = rec->next; + } + } + // PYPY segfaults here when passing builtin function like sum. + // Raising an fail exception here works to prevent the segfault, but + // only on gcc. See PR #1413 for full details + } + + // !MO2! - check arity + + if (!mo2::python::detail::has_compatible_arity(func, sizeof...(Args))) { + return false; + } + + // !MO2! - everything below is copy/paste from pybind11 + + // ensure GIL is held during functor destruction + struct func_handle { + function f; +#if !(defined(_MSC_VER) && _MSC_VER == 1916 && defined(PYBIND11_CPP17)) + // This triggers a syntax error under very special conditions (very + // weird indeed). + explicit +#endif + func_handle(function&& f_) noexcept + : f(std::move(f_)) + { + } + func_handle(const func_handle& f_) { operator=(f_); } + func_handle& operator=(const func_handle& f_) + { + gil_scoped_acquire acq; + f = f_.f; + return *this; + } + ~func_handle() + { + gil_scoped_acquire acq; + function kill_f(std::move(f)); + } + }; + + // to emulate 'move initialization capture' in C++11 + struct func_wrapper { + func_handle hfunc; + explicit func_wrapper(func_handle&& hf) noexcept : hfunc(std::move(hf)) + { + } + Return operator()(Args... args) const + { + gil_scoped_acquire acq; + object retval(hfunc.f(std::forward(args)...)); + return retval.template cast(); + } + }; + + value = func_wrapper(func_handle(std::move(func))); + return true; + } + + template + static handle cast(Func&& f_, return_value_policy policy, handle /* parent */) + { + if (!f_) { + return none().inc_ref(); + } + + auto result = f_.template target(); + if (result) { + return cpp_function(*result, policy).release(); + } + return cpp_function(std::forward(f_), policy).release(); + } + + PYBIND11_TYPE_CASTER(type, const_name("Callable[[") + + concat(make_caster::name...) + + const_name("], ") + + make_caster::name + + const_name("]")); + }; + +} // namespace pybind11::detail + +#endif diff --git a/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/generator.h b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/generator.h new file mode 100644 index 0000000..cbf9d18 --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/generator.h @@ -0,0 +1,57 @@ +#ifndef PYTHON_PYBIND11_GENERATOR_H +#define PYTHON_PYBIND11_GENERATOR_H + +#include + +#include + +namespace mo2::python { + + // the code here is mostly taken from pybind11 itself, and relies on some pybind11 + // internals so might be subject to change when upgrading pybind11 versions + + namespace detail { + template + struct generator_state { + std::generator g; + decltype(g.begin()) it; + + generator_state(std::generator gen) : g(std::move(gen)), it(g.begin()) {} + }; + } // namespace detail + + // create a Python generator from a C++ generator + // + template + auto make_generator(std::generator g, Args&&... args) + { + using state = detail::generator_state; + + namespace py = pybind11; + if (!py::detail::get_type_info(typeid(state), false)) { + py::class_(py::handle(), "iterator", pybind11::module_local()) + .def("__iter__", + [](state& s) -> state& { + return s; + }) + .def( + "__next__", + [](state& s) -> T { + if (s.it != s.g.end()) { + T v = *s.it; + s.it++; + return v; + } + else { + throw py::stop_iteration(); + } + }, + std::forward(args)...); + } + + return py::cast(state{std::move(g)}); + } + +} // namespace mo2::python + +#endif diff --git a/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/shared_cpp_owner.h b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/shared_cpp_owner.h new file mode 100644 index 0000000..02c2111 --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/shared_cpp_owner.h @@ -0,0 +1,90 @@ +#ifndef PYTHON_PYBIND11_SHARED_CPP_OWNER_H +#define PYTHON_PYBIND11_SHARED_CPP_OWNER_H + +#include + +// pybind11 has some issues when a Python classes extend a C++ wrapper since the Python +// object is not kept alive alongside the returned object +// +// there is a pybind11 branch called "smart_holder" that tries to solve this in a very +// complicated way (with many other features) +// +// here, we simply use a custom type_caster<> for the classes we need - see the actual +// definition in mo2::python::detail below +// +// IMPORTANT: this only works for classes that are managed by shared_ptr on the C++ +// side, not Qt object (see pybind11-qt holder for that) +// + +namespace mo2::python::detail { + + template + struct shared_cpp_owner_caster + : pybind11::detail::copyable_holder_caster { + + // note that the actual holder type might be different in term of constness + using type = Type; + using holder_type = SharedType; + + using base = pybind11::detail::copyable_holder_caster; + using base::holder; + using base::value; + + // in load, we use the default type_caster<> to extract the shared pointer, then + // we replace it by a custom one + // + // the custom shared_ptr<> holds the py::object BUT does not really manage the + // C++ object because it will ref-count but not delete it + // + // this should work because here it's how it works: + // - the Python object holds a standard shared_ptr<> for the C++ object -> the + // C++ object remains alive as long as the Python one remains alive + // - the C++ object holds a shared_ptr<> that manages the python object -> the + // Python object remains alive as-long as there is a shared_ptr<> on the C++ + // side + // + bool load(pybind11::handle src, bool convert) + { + namespace py = pybind11; + + if (!base::load(src, convert)) { + return false; + } + + holder.reset(holder.get(), [pyobj = py::reinterpret_borrow( + src)](auto*) mutable { + py::gil_scoped_acquire s; + pyobj = py::object(); + + // we do NOT delete the object here - if this was the last reference to + // the Python object, the Python object will delete it + }); + + return true; + } + + // cast simply forward to the original type_caster<> + // + static pybind11::handle cast(const holder_type& src, + pybind11::return_value_policy policy, + pybind11::handle parent) + { + return base::cast(src, policy, parent); + } + }; + +} // namespace mo2::python::detail + +#define MO2_PYBIND11_SHARED_CPP_HOLDER(Type) \ + namespace pybind11::detail { \ + template <> \ + struct type_caster> \ + : mo2::python::detail::shared_cpp_owner_caster> {}; \ + template <> \ + struct type_caster> \ + : mo2::python::detail::shared_cpp_owner_caster< \ + Type, std::shared_ptr> {}; \ + } + +#endif diff --git a/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant.h b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant.h new file mode 100644 index 0000000..bd7bd9b --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant.h @@ -0,0 +1,53 @@ +#ifndef PYTHON_PYBIND11_UTILS_SMART_VARIANT_H +#define PYTHON_PYBIND11_UTILS_SMART_VARIANT_H + +#include + +namespace mo2::python { + + namespace detail { + + // simple template class that should be specialized to expose proper fromXXX + // methods + // + template + struct smart_variant_converter { + template + static T from(U&& u) + { + return T{std::forward(u)}; + } + }; + + } // namespace detail + + // a smart_variant is a std::variant that can be automatically converted to any of + // its type via custom operator T() + // + // user should specialize detail::smart_variant_converter to provide proper + // conversions + // + template + struct smart_variant : std::variant { + using std::variant::variant; + + template ...>, int> = 0> + operator T() const + { + return std::visit( + [](auto const& t) -> T { + if constexpr (std::is_same_v, T>) { + return t; + } + else { + return detail::smart_variant_converter::from(t); + } + }, + *this); + } + }; + +} // namespace mo2::python + +#endif diff --git a/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant_wrapper.h b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant_wrapper.h new file mode 100644 index 0000000..123d95c --- /dev/null +++ b/libs/plugin_python/src/pybind11-utils/include/pybind11_utils/smart_variant_wrapper.h @@ -0,0 +1,180 @@ +#ifndef PYTHON_PYBIND11_UTILS_SMART_VARIANT_WRAPPER_H +#define PYTHON_PYBIND11_UTILS_SMART_VARIANT_WRAPPER_H + +#include +#include + +#include +#include + +#include "smart_variant.h" + +namespace mo2::python { + + namespace detail { + + // simple helper class that expose a ::type attribute which is U is I is in Is, + // V otherwise + template + struct wrap_arg; + + template + struct wrap_arg> { + using type = + std::conditional_t...>, + U, V>; + }; + + // helper type for wrap_arg + template + using wrap_arg_t = typename wrap_arg::type; + + template + auto wrap_arguments_impl(std::index_sequence, Fn&& fn, R (*)(Args...), + std::index_sequence) + { + return [fn = std::forward(fn)]( + wrap_arg_t>... args) { + return std::invoke(fn, std::forward(args)...); + }; + } + + template + auto make_convertible_index_sequence(std::index_sequence) + { + return std::index_sequence<(std::is_convertible_v ? Is : -1)...>{}; + } + + template + auto wrap_arguments_impl(Fn&& fn, R (*sg)(Args...)) + { + if constexpr (sizeof...(Is) == 0) { + return wrap_arguments_impl( + make_convertible_index_sequence( + std::make_index_sequence{}), + std::forward(fn), sg, + std::make_index_sequence{}); + } + else { + return wrap_arguments_impl( + std::index_sequence{}, std::forward(fn), sg, + std::make_index_sequence{}); + } + } + + template + auto wrap_return_impl(Fn&& fn, R (*)(Args...)) + { + return [fn = std::forward(fn)](Args... args) { + return T{std::invoke(fn, std::forward(args)...)}; + }; + } + + // make_python_function_signature: return a null-pointer with the proper type + // for the given function + + template + struct function_signature { + using type = + pybind11::detail::function_signature_t>; + }; + + template + struct function_signature { + using type = R(Args...); + }; + + template + struct function_signature { + using type = R(C*, Args...); + }; + + template + struct function_signature { + using type = R(C*, Args...); + }; + + template + struct function_signature { + using type = R(const C*, Args...); + }; + + template + struct function_signature { + using type = R(const C*, Args...); + }; + + template + using function_signature_t = typename function_signature::type; + + template + class wrap_type_caster { + using variant_type = std::variant; + using variant_caster = pybind11::detail::make_caster; + + public: + PYBIND11_TYPE_CASTER(Type, variant_caster::name); + + bool load(pybind11::handle src, bool convert) + { + variant_caster caster; + + if (!caster.load(src, convert)) { + return false; + } + + value = std::visit( + [](auto const& fn) { + return Type(fn); + }, + static_cast(caster)); + return true; + } + + static pybind11::handle cast(const Type& src, + pybind11::return_value_policy policy, + pybind11::handle parent) + { + return variant_caster::cast(variant_type(std::in_place_index<0>, src), + policy, parent); + } + }; + + } // namespace detail + + // wrap the given function-like object to accept T instead of the specified + // arguments at the specified positions + // + // if the list of positions is empty, replace all arguments that can be converted to + // T + // + template + auto wrap_arguments(Fn&& fn) + { + return detail::wrap_arguments_impl( + std::forward(fn), + (mo2::python::detail::function_signature_t*)nullptr); + } + + // wrap the given function-like object to return T instead of the specified type + // + template + auto wrap_return(Fn&& fn) + { + return detail::wrap_return_impl( + std::forward(fn), + (mo2::python::detail::function_signature_t*)nullptr); + } + +} // namespace mo2::python + +namespace pybind11::detail { + + template + struct type_caster<::mo2::python::smart_variant> + : variant_caster<::mo2::python::smart_variant> {}; + +} // namespace pybind11::detail + +#endif -- cgit v1.3.1