Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ option(NB_TEST_STABLE_ABI "Test the stable ABI interface?" OFF)
option(NB_TEST_SHARED_BUILD "Build a shared nanobind library for the test suite?" OFF)
option(NB_TEST_CUDA "Force the use of the CUDA/NVCC compiler for testing purposes" OFF)
option(NB_TEST_FREE_THREADED "Build free-threaded extensions for the test suite?" ON)
option(NB_TEST_NO_INTEROP "Build without framework interoperability support?" OFF)

if (NOT MSVC)
option(NB_TEST_SANITIZERS_ASAN "Build tests with the address sanitizer?" OFF)
Expand Down
11 changes: 10 additions & 1 deletion cmake/nanobind-config.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ function (nanobind_build_library TARGET_NAME)
${NB_DIR}/src/nb_static_property.cpp
${NB_DIR}/src/nb_ft.h
${NB_DIR}/src/nb_ft.cpp
${NB_DIR}/src/nb_foreign.cpp
${NB_DIR}/src/common.cpp
${NB_DIR}/src/error.cpp
${NB_DIR}/src/trampoline.cpp
Expand Down Expand Up @@ -236,6 +237,10 @@ function (nanobind_build_library TARGET_NAME)
target_compile_definitions(${TARGET_NAME} PUBLIC NB_FREE_THREADED)
endif()

if (TARGET_NAME MATCHES "-local")
target_compile_definitions(${TARGET_NAME} PRIVATE NB_DISABLE_INTEROP)
endif()

# Nanobind performs many assertion checks -- detailed error messages aren't
# included in Release/MinSizeRel/RelWithDebInfo modes
target_compile_definitions(${TARGET_NAME} PRIVATE
Expand Down Expand Up @@ -330,7 +335,7 @@ endfunction()

function(nanobind_add_module name)
cmake_parse_arguments(PARSE_ARGV 1 ARG
"STABLE_ABI;FREE_THREADED;NB_STATIC;NB_SHARED;PROTECT_STACK;LTO;NOMINSIZE;NOSTRIP;MUSL_DYNAMIC_LIBCPP;NB_SUPPRESS_WARNINGS"
"STABLE_ABI;FREE_THREADED;NB_STATIC;NB_SHARED;PROTECT_STACK;LTO;NOMINSIZE;NOSTRIP;MUSL_DYNAMIC_LIBCPP;NB_SUPPRESS_WARNINGS;NO_INTEROP"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A general question is whether this feature should be opt-in or opt-out. Given that it adds overheads (even if small), my tendency would be to make it opt-in. (e.g. INTEROP instead of NO_INTEROP)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the feature becomes opt-in, would you reverse the polarity of the macro as well? In other words, NB_DISABLE_FOREIGN becomes NB_ENABLE_FOREIGN.
Obviously, other build systems do not use nanobind-config.cmake. By default, any macros you add would not be defined. Developers would opt-in by defining the new macro.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just chiming in for another vote for opt-in. I imagine that most projects don't need to pay the cost (as the bindings will be self contained), and the ones that do would probably just use it to use as a transition period and then turn it off again.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The authors of a particular extension module don't generally know when they build it whether anyone will want to use its types from a different framework (or a different ABI version of the same framework). I think this is what pybind11 was referring to in their rationale for adding the _pybind11_conduit_v1_ methods unconditionally -- "to avoid "oh, too late!" situations" (pybind/pybind11#5296). I'm happy to switch the default, but I wonder if we might want to leave this question open until we have a better quantification of the cost? Speaking of which, @wjakob if you still have a copy of the benchmark that you used to obtain the performance comparison numbers in the nanobind docs, I think that might be useful here.

"NB_DOMAIN" "")

add_library(${name} MODULE ${ARG_UNPARSED_ARGUMENTS})
Expand Down Expand Up @@ -375,6 +380,10 @@ function(nanobind_add_module name)
set(libname "${libname}-ft")
endif()

if (ARG_NO_INTEROP)
set(libname "${libname}-local")
endif()

if (ARG_NB_DOMAIN AND ARG_NB_SHARED)
set(libname ${libname}-${ARG_NB_DOMAIN})
endif()
Expand Down
4 changes: 4 additions & 0 deletions docs/api_cmake.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ The high-level interface consists of just one CMake command:
an optimization that nanobind does by default in this specific case).
If this explanation sounds confusing, then you can ignore it. See the
detailed description below for more information on this step.
* - ``NO_INTEROP``
- Remove support for :ref:`interoperability with other Python binding
frameworks <interop>`. If you don't need it in your environment, this
offers a minor performance and code size benefit.

:cmake:command:`nanobind_add_module` performs the following
steps to produce bindings.
Expand Down
92 changes: 92 additions & 0 deletions docs/api_core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3142,12 +3142,24 @@ Global flags
functions are still alive when the Python interpreter shuts down. Call this
function to disable or re-enable leak warnings.

The configuration affected by this function is global to a :ref:`nanobind
domain <type-visibility>`. If you use it, you are encouraged to isolate your
extension from others by passing the ``NB_DOMAIN`` parameter to the
:cmake:command:`nanobind_add_module()` CMake command, so that your choices
don't need to impact unrelated extensions.

.. cpp:function:: void set_implicit_cast_warnings(bool value) noexcept

By default, nanobind loudly complains when it attempts to perform an
implicit conversion, and when that conversion is not successful. Call this
function to disable or re-enable the warnings.

The configuration affected by this function is global to a :ref:`nanobind
domain <type-visibility>`. If you use it, you are encouraged to isolate your
extension from others by passing the ``NB_DOMAIN`` parameter to the
:cmake:command:`nanobind_add_module()` CMake command, so that your choices
don't need to impact unrelated extensions.

.. cpp:function:: inline bool is_alive() noexcept

The function returns ``true`` when nanobind is initialized and ready for
Expand All @@ -3156,6 +3168,86 @@ Global flags
this liveness status can be useful to avoid operations that are illegal in
the latter context.

Interoperability
----------------

See the :ref:`separate interoperability documentation <interop>` for important
additional information and caveats about this feature.

.. cpp:function:: void interoperate_by_default(bool export_all = true, bool import_all = true)

Configure whether this :ref:`nanobind domain <type-visibility>` should exchange
type information with other binding libraries (and other nanobind domains)
so that functions bound in one can accept and return objects whose types are
bound in another. The default, if :cpp:func:`interoperate_by_default` is not
called, is to exchange such information only when requested on a per-type
basis via calls to :cpp:func:`import_for_interop` or
:cpp:func:`export_for_interop`.

By specifying arguments to :cpp:func:`interoperate_by_default`, it is
possible to separately configure whether to enable automatic sharing of
our types with others (*export_all*) or automatic use of types shared by
others (*import_all*). Once either type of automatic sharing is enabled,
it remains enabled for the lifetime of the Python interpreter.

Automatic export is equivalent to calling :cpp:func:`export_for_interop` on
each type produced by a :cpp:class:`nb::class_` or :cpp:struct:`nb::enum_`
binding statement in this nanobind domain.

Automatic import is equivalent to calling :cpp:func:`import_for_interop` on
each type exported by a different binding library or nanobind domain, as
long as that library is written in C++ and uses a compatible platform ABI.
In order to interoperate with a type binding that was created in another
language, including pure C, you must still make an explicit call to
:cpp:func:`import_for_interop`, providing a template argument so that
nanobind can tell which C++ type should be associated with it.

Enabling automatic export or import affects both types that have already been
bound and types that are yet to be bound.

The configuration affected by this function is global to a nanobind
domain. If you use it, you are encouraged to isolate your extension from
others by passing the ``NB_DOMAIN`` parameter to the
:cmake:command:`nanobind_add_module()` CMake command, so that your choices
don't need to impact unrelated extensions.

.. cpp:function:: template <typename T = void> void import_for_interop(handle type)

Make the Python type object *type*, which was bound using a different binding
framework (or different nanobind domain) and then exported using a facility
like our :cpp:func:`export_for_interop`, be available so that functions bound
in this nanobind domain can accept and return its instances using the
C++ type ``T``, or the C++ type that was specified when *type* was bound.

If no template argument is specified, then *type* must have been bound by
another C++ binding library that uses the same :ref:`platform ABI
<type-visibility>` as us; the C++ type will be determined by inspecting its
binding.

If a template argument is specified, then nanobind associates that C++ type
with the given Python type, trusting rather than verifying that they match.
The C++ type ``T`` must perfectly match the memory layout and ABI of the
structure used by whoever bound the Python *type*.
This is the only way to import types written in other languages than C++
(including those in plain C), since nanobind needs a ``std::type_info`` for
its type lookups and non-C++ bindings don't provide those directly.

Repeatedly importing the same Python type as the same C++ type is idempotent.
Importing the same Python type as multiple different C++ types will fail.

A descriptive C++ exception will be thrown if the import fails.

.. cpp:function:: void export_for_interop(handle type)

Make the Python type object *type*, which was created by a
:cpp:class:`nb::class_` or :cpp:class:`nb::enum_` binding statement in this
nanobind domain be available for import by other binding libraries and other
nanobind domains. If they do so, then their bound functions will be able to
accept and return instances of *type*.

Repeatedly exporting the same type is idempotent.


Miscellaneous
-------------

Expand Down
21 changes: 19 additions & 2 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ case, both modules must use the same nanobind ABI version, or they will be
isolated from each other. Releases that don't explicitly mention an ABI version
below inherit that of the preceding release.

Version TBD (unreleased)
------------------------

.. TODO: update the pybind11 version number below before releasing

- nanobind has adopted the new `pymetabind
<https://github.com/hudson-trading/pymetabind>`__ standard for interoperating
with other Python binding libraries (including other ABI versions of
nanobind). When the interoperability feature is activated, which in most cases
is as simple as writing :cpp:func:`nb::interoperate_by_default()
<interoperate_by_default>`, a function or method that is bound using nanobind
can accept and return values of types that were bound using other binding
libraries that support pymetabind, notably including pybind11 versions !TBD!
and later. This feature is likely to be of **great utility** to anyone who
is working on porting large or interconnected extension modules from pybind11
to nanobind. See the extensive :ref:`interoperability documentation <interop>`
for more details. (PR `#1140 <https://github.com/wjakob/nanobind/pull/1140>`__).

Version 2.9.2 (Sep 4, 2025)
---------------------------

Expand Down Expand Up @@ -103,7 +121,6 @@ Version 2.9.0 (Sep 4, 2025)
`#1132 <https://github.com/wjakob/nanobind/pull/1132>`__,
`#1090 <https://github.com/wjakob/nanobind/pull/1090>`__).


Version 2.8.0 (July 16, 2025)
-----------------------------

Expand Down Expand Up @@ -383,7 +400,7 @@ Version 2.3.0

There is no version 2.3.0 due to a deployment mishap.

- Added casters for `Eigen::Map<Eigen::SparseMatrix<...>` types from the `Eigen library
- Added casters for ``Eigen::Map<Eigen::SparseMatrix<...>`` types from the `Eigen library
<https://eigen.tuxfamily.org/index.php?title=Main_Page>`__. (PR `#782
<https://github.com/wjakob/nanobind/pull/782>`_).

Expand Down
94 changes: 71 additions & 23 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -233,33 +233,35 @@ will:

.. _type-visibility:

How can I avoid conflicts with other projects using nanobind?
-------------------------------------------------------------

Suppose that a type binding in your project conflicts with another extension, for
example because both expose a common type (e.g., ``std::latch``). nanobind will
warn whenever it detects such a conflict:
How can I control whether two extension modules see each other's types?
-----------------------------------------------------------------------

nanobind creates a variety of internal data structures to support the bindings
you ask it to make. These are not isolated to a single extension module,
because it's useful for large binding projects to be able to split related
bindings into multiple extensions without losing their ability to work with
one another's types. Instead, extension modules are divided into nanobind
*domains* based on two attributes: an automatically determined string (the
nanobind "ABI tag") that captures compatibility-relevant aspects of their
build environments and nanobind versions, and an ``NB_DOMAIN`` string that
may be provided at build time (as shown below).
If multiple extension modules share the same ABI tag and the same ``NB_DOMAIN``
string, they will wind up in the same nanobind domain, which allows them to
work together exactly as if they were one big extension.

Sometimes, this causes problems. For example, you might expose a binding
for a commonly used type (such as ``std::latch``) that some other nanobind
extension in your Python interpreter also happens to provide a binding for.
nanobind will warn whenever it detects such a conflict:

.. code-block:: text

RuntimeWarning: nanobind: type 'latch' was already registered!

In the worst case, this could actually break both packages (especially if the
bindings of the two packages expose an inconsistent/incompatible API).

The higher-level issue here is that nanobind will by default try to make type
bindings visible across extensions because this is helpful to partition large
binding projects into smaller parts. Such information exchange requires that
the extensions:

- use the same nanobind *ABI version* (see the :ref:`Changelog <changelog>` for details).
- use the same compiler (extensions built with GCC and Clang are isolated from each other).
- use ABI-compatible versions of the C++ library.
- use the stable ABI interface consistently (stable and unstable builds are isolated from each other).
- use debug/release mode consistently (debug and release builds are isolated from each other).

In addition, nanobind provides a feature to intentionally scope extensions to a
named domain to avoid conflicts with other extensions. To do so, specify the
bindings of the two packages expose an inconsistent/incompatible API). So it's
useful to be able to enforce a boundary between extensions sometimes, even if
they would otherwise be ABI-compatible. To do so, you can specify the
``NB_DOMAIN`` parameter in CMake:

.. code-block:: cmake
Expand All @@ -268,8 +270,54 @@ named domain to avoid conflicts with other extensions. To do so, specify the
NB_DOMAIN my_project
my_ext.cpp)

In this case, inter-extension type visibility is furthermore restricted to
extensions in the ``"my_project"`` domain.
Two extensions can only be in the same nanobind domain if either they both
specify the same value for that parameter (``"my_project"`` in this case) or
neither one specifies the parameter.

As mentioned above, two extensions can also only be in the same nanobind domain
if they share the same ABI tag. This is determined in two parts, as follows:

- They must use compatible *platform ABI*, so that (for example) a
``std::vector<int>`` created in one can be safely used in the other.
That means:

- They must use the same C++ standard library (MSVC, libc++, or libstdc++),
and the same ABI version of it. For example, extensions that use libstdc++
must match in terms of whether they use the pre- or post-C++11 ABI, and
extensions that use libc++ must use the same `libc++ ABI version
<https://libcxx.llvm.org/DesignDocs/ABIVersioning.html>`__.

- On Windows, they must use the same compiler (MSVC, mingw, or cygwin).

- If compiled using MSVC, they must use the same major version of the
compiler, the same multi-threading style (dynamic ``/MD``,
static ``/MT``, or single-threaded), and they must match in terms of
whether or not they are built in debugging mode.

- They must use compatible *nanobind ABI*, so that the nanobind internal data
structures created in one can be safely used in the other. That means:

- They must use the same nanobind *ABI version*; see the
:ref:`Changelog <changelog>` for details.

- They must be consistent in their use of Python's stable ABI: either
both built against the stable ABI (cmake ``STABLE_ABI`` flag) or both not.

- They must be consistent in their use of free-threading: either both
built with free-threading support (cmake ``FREE_THREADED`` flag) or both not.

- They must either both use released versions of nanobind or both be built
from Git development snapshots, rather than a mix of the two.

If you want to share some types between two extensions that have the same
platform ABI (the first category in the above list), but are in different
nanobind domains due to using different nanobind ABI or different specified
``NB_DOMAIN`` strings, all is not lost! The :ref:`interoperability support
<interop>` between nanobind and other binding libraries also provides
interoperability between different nanobind domains, as long as the platform
ABI matches. It must be specifically enabled, and there are a few things it
can't do, but for most purposes it's hard to tell the difference from operating
within the same domain. See the linked documentation for more details.

Can I use nanobind without RTTI or C++ exceptions?
--------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ The nanobind logo was designed by `AndoTwin Studio
packaging
typing
utilities
interop

.. toctree::
:caption: Advanced
Expand Down
4 changes: 2 additions & 2 deletions docs/refleaks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ easily disable them by calling :cpp:func:`nb::set_leak_warnings()
}

Note that is a *global flag* shared by all nanobind extension libraries in the
same ABI domain. When changing global flags, please isolate your extension from
others by passing the ``NB_DOMAIN`` parameter to the
same `nanobind domain <type-visibility>`. When changing global flags, please
isolate your extension from others by passing the ``NB_DOMAIN`` parameter to the
:cmake:command:`nanobind_add_module()` CMake command:

.. code-block:: cmake
Expand Down
5 changes: 4 additions & 1 deletion include/nanobind/nb_attr.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,10 @@ enum cast_flags : uint8_t {
// This implies that objects added to the cleanup list may be
// released immediately after the caster's final output value is
// obtained, i.e., before it is used.
manual = (1 << 3)
manual = (1 << 3),

// Disallow satisfying this cast with a foreign framework's binding
not_foreign = (1 << 4),
};


Expand Down
9 changes: 6 additions & 3 deletions include/nanobind/nb_cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,15 +189,16 @@ struct type_caster<T, enable_if_t<std::is_arithmetic_v<T> && !is_std_char_v<T>>>

template <typename T>
struct type_caster<T, enable_if_t<std::is_enum_v<T>>> {
NB_INLINE bool from_python(handle src, uint8_t flags, cleanup_list *) noexcept {
NB_INLINE bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept {
int64_t result;
bool rv = enum_from_python(&typeid(T), src.ptr(), &result, flags);
bool rv = enum_from_python(&typeid(T), src.ptr(), &result, sizeof(T),
flags, cleanup);
value = (T) result;
return rv;
}

NB_INLINE static handle from_cpp(T src, rv_policy, cleanup_list *) noexcept {
return enum_from_cpp(&typeid(T), (int64_t) src);
return enum_from_cpp(&typeid(T), (int64_t) src, sizeof(T));
}

NB_TYPE_CASTER(T, const_name<T>())
Expand Down Expand Up @@ -504,6 +505,8 @@ template <typename Type_> struct type_caster_base : type_caster_base_tag {
} else {
const std::type_info *type_p =
(!has_type_hook && ptr) ? &typeid(*ptr) : nullptr;
if (type_p && (void *) ptr != dynamic_cast<void *>(ptr))
type_p = nullptr; // don't try to downcast from a non-primary base
return nb_type_put_p(type, type_p, ptr, policy, cleanup);
}
}
Expand Down
Loading