Skip to content
Merged
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
32 changes: 32 additions & 0 deletions docs/config/autowrap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,38 @@ Semiwrap emits ``py::kw_only()`` before the first exposed argument that retains
a default. An argument with ``param_override.no_default: true`` does not trigger
the keyword-only marker.

Overload binding order
----------------------

When multiple overloads can accept the same Python arguments, pybind11 tries
matching overloads in registration order within each overload-resolution pass.
Use ``binding_order`` to register a preferred overload earlier:

.. code-block:: yaml

classes:
MyClass:
methods:
my_method:
overloads:
SomeType:
binding_order: -1
OtherType:

Lower integers bind first. The default is ``0``, so negative values move an
overload earlier and positive values move it later. Equal values preserve the
existing binding order; the order of entries in YAML does not affect it.

The setting also works for constructors, static and exposed protected methods,
and free functions under ``functions``. It can be set on a function or method
as a default for its overloads; an overload's explicit value (including ``0``)
overrides that default.

Ordering applies within a class (including public and exposed protected methods)
or among free functions generated from one header. It does not reorder classes,
headers, or handwritten ``inline_code`` bindings, and does not change pybind11's
preference for matches that require no argument conversion.

Name transforms
---------------

Expand Down
3 changes: 3 additions & 0 deletions src/semiwrap/autowrap/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ class FunctionContext:
# User settings from autowrap_yml.FunctionData
#

#: Lower integers are registered first; ties preserve existing order
binding_order: int

#: If True, don't wrap this, but provide a pure virtual implementation
ignore_pure: bool

Expand Down
1 change: 1 addition & 0 deletions src/semiwrap/autowrap/cxxparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1575,6 +1575,7 @@ def _on_fn_or_method(
# info
# vararg=fn.vararg,
# user settings
binding_order=data.binding_order if data.binding_order is not None else 0,
ignore_pure=data.ignore_pure,
ignore_py=data.ignore_py,
cpp_code=data.cpp_code,
Expand Down
12 changes: 7 additions & 5 deletions src/semiwrap/autowrap/render_pybind11.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,12 +440,14 @@ def cls_def(r: RenderBuffer, cls: ClassContext, varname: str):
if cls.add_default_constructor:
r.writeln(f"{varname}.def(py::init<>(), release_gil());")

for fn in cls.wrapped_public_methods:
genmethod(r, varname, cls.full_cpp_name, fn, None)

methods: T.List[T.Tuple[FunctionContext, T.Optional[str]]] = [
(fn, None) for fn in cls.wrapped_public_methods
]
if cls.trampoline is not None:
for fn in cls.wrapped_protected_methods:
genmethod(r, varname, cls.full_cpp_name, fn, cls.trampoline.var)
methods.extend((fn, cls.trampoline.var) for fn in cls.wrapped_protected_methods)

for fn, trampoline in sorted(methods, key=lambda method: method[0].binding_order):
genmethod(r, varname, cls.full_cpp_name, fn, trampoline)

for prop in cls.public_properties:
_genprop(r, varname, cls.full_cpp_name, prop)
Expand Down
5 changes: 4 additions & 1 deletion src/semiwrap/autowrap/render_wrapped.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,10 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str:
# Global methods
if hctx.functions:
r.writeln()
for index, fn in enumerate(hctx.functions, start=1):
for index, fn in sorted(
enumerate(hctx.functions, start=1),
key=lambda item: item[1].binding_order,
):
if not fn.ignore_py:
r.writeln(
f"{_absolute_qualname(_function_helper_qualname(hctx, fn, index))}"
Expand Down
6 changes: 6 additions & 0 deletions src/semiwrap/config/autowrap_yml.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ class OverloadData:
#: ``defaults.default_args_as_kw_only``.
default_args_as_kw_only: Optional[bool] = None

#: Binding priority: lower integers are registered with pybind11 first.
#: Ties preserve the existing order. Inherits the function/method setting
#: when omitted, otherwise defaults to 0. Applies within a class or within
#: the free functions of a single header, not across headers or classes.
binding_order: Optional[int] = None

#: If True, prepends an underscore to the python name
internal: bool = False

Expand Down
105 changes: 105 additions & 0 deletions tests/cpp/sw-test/semiwrap/ft/overloads.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,109 @@
functions:
bindingOrder:
overloads:
int:
long:
binding_order: -1
bindingOrderUnchanged:
overloads:
long:
int:
bindingOrderLater:
overloads:
int:
binding_order: 3
long:
bindingOrderTied:
overloads:
int, int:
long long, int:
binding_order: -2
long:
binding_order: -2
bindingOrderInherited:
binding_order: 2
overloads:
int, int, int:
long, int, int:
long long, int:
binding_order: 0
short:
binding_order: -1
fnOverload:
overloads:
int, int:
int:
classes:
BindingOrder:
attributes:
selected:
methods:
BindingOrder:
overloads:
int:
binding_order: 1
long:
choose:
overloads:
int:
long:
binding_order: -1
unchanged:
overloads:
int:
long:
chooseStatic:
overloads:
int:
long:
binding_order: -1
later:
overloads:
int:
binding_order: 3
long:
tied:
overloads:
int, int:
long long, int:
binding_order: -2
long:
binding_order: -2
inherited:
binding_order: 2
overloads:
int, int, int:
long, int, int:
long long, int:
binding_order: 0
short:
binding_order: -1
mixedUnchanged:
rename: mixedUnchanged
overloads:
long:
int:
mixedPrioritized:
rename: mixedPrioritized
overloads:
long:
binding_order: -1
int:
chooseProtected:
overloads:
int:
long:
binding_order: -1
BindingOrderTemplate:
template_params:
- T
methods:
choose:
binding_order: 2
overloads:
T:
long:
binding_order: 0
OverloadedObject:
methods:
overloaded:
Expand Down Expand Up @@ -31,3 +131,8 @@ classes:
overloaded_private:
overloads:
int:
templates:
BindingOrderTemplateInt:
qualname: BindingOrderTemplate
params:
- int
68 changes: 66 additions & 2 deletions tests/cpp/sw-test/src/swtest/ft/include/overloads.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,75 @@
#pragma once

int fnOverload(int i, int j)
// Template bindings include this header in multiple translation units.
inline int bindingOrder(int value) { return 1; }
inline int bindingOrder(long value) { return 2; }

inline int bindingOrderUnchanged(int value) { return 1; }
inline int bindingOrderUnchanged(long value) { return 2; }

inline int bindingOrderLater(int value) { return 1; }
inline int bindingOrderLater(long value) { return 2; }

inline int bindingOrderTied(int value, int skip = 0) { return 1; }
inline int bindingOrderTied(long value) { return 2; }
inline int bindingOrderTied(long long value, int skip = 0) { return 3; }

inline int bindingOrderInherited(int value, int subset = 0, int inherited = 0) { return 1; }
inline int bindingOrderInherited(long value, int subset = 0, int inherited = 0) { return 2; }
inline int bindingOrderInherited(long long value, int subset = 0) { return 3; }
inline int bindingOrderInherited(short value) { return 4; }

struct BindingOrder {
BindingOrder(int value) : selected(1) {}
BindingOrder(long value) : selected(2) {}
virtual ~BindingOrder() = default;

int choose(int value) { return 1; }
int choose(long value) { return 2; }
int unchanged(int value) { return 1; }
int unchanged(long value) { return 2; }
static int chooseStatic(int value) { return 1; }
static int chooseStatic(long value) { return 2; }

int later(int value) { return 1; }
int later(long value) { return 2; }

int tied(int value, int skip = 0) { return 1; }
int tied(long value) { return 2; }
int tied(long long value, int skip = 0) { return 3; }

int inherited(int value, int subset = 0, int inherited = 0) { return 1; }
int inherited(long value, int subset = 0, int inherited = 0) { return 2; }
int inherited(long long value, int subset = 0) { return 3; }
int inherited(short value) { return 4; }

int selected;

protected:
int chooseProtected(int value) { return 1; }
int chooseProtected(long value) { return 2; }

// Declared before public overloads to test the existing public-first order.
int mixedUnchanged(long value) { return 2; }
int mixedPrioritized(long value) { return 2; }

public:
int mixedUnchanged(int value) { return 1; }
int mixedPrioritized(int value) { return 1; }
};

template <typename T>
struct BindingOrderTemplate {
int choose(T value) { return 1; }
int choose(long value) { return 2; }
};

inline int fnOverload(int i, int j)
{
return j;
}

int fnOverload(int i)
inline int fnOverload(int i)
{
return i;
}
Expand Down
58 changes: 58 additions & 0 deletions tests/test_binding_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import pytest

from swtest.ft import _ft as ft


@pytest.fixture(params=["function", "method"])
def overloads(request):
if request.param == "function":
return (
ft.bindingOrderUnchanged,
ft.bindingOrderLater,
ft.bindingOrderTied,
ft.bindingOrderInherited,
)
obj = ft.BindingOrder(42)
return obj.unchanged, obj.later, obj.tied, obj.inherited


def test_binding_order_defaults_preserve_declaration_order(overloads):
unchanged, _, _, _ = overloads
assert unchanged(42) == 1


def test_positive_binding_order_moves_overload_later(overloads):
_, later, _, _ = overloads
assert later(42) == 2


def test_equal_priorities_preserve_declaration_order(overloads):
_, _, tied, _ = overloads
# The second and third overloads have the same negative priority.
assert tied(42) == 2
# Exclude the second overload: the third still precedes the first.
assert tied(42, skip=0) == 3


def test_binding_order_inheritance_and_explicit_zero(overloads):
_, _, _, inherited = overloads
assert inherited(42) == 4 # Explicit -1 precedes explicit 0 and inherited 2.
# Exclude the fourth overload: explicit 0 overrides the inherited 2.
assert inherited(42, subset=0) == 3
# Exclude both overrides: equal inherited priorities retain original order.
assert inherited(42, inherited=0) == 1


def test_template_method_binding_order():
assert ft.BindingOrderTemplateInt().choose(42) == 2


def test_equal_priorities_preserve_public_before_protected_order():
# The protected overload is declared first, but public bindings came first
# before binding_order was introduced. Both are renamed to the same Python
# name so this tests actual overload resolution across the access boundary.
assert ft.BindingOrder(42).mixedUnchanged(42) == 1


def test_protected_overload_can_precede_public_overload():
assert ft.BindingOrder(42).mixedPrioritized(42) == 2
12 changes: 11 additions & 1 deletion tests/test_ft_overloads.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
from swtest import ft
from swtest.ft import _ft as ft


def test_binding_order_resolves_ambiguous_overloads():
assert ft.bindingOrder(42) == 2
obj = ft.BindingOrder(42)
assert obj.selected == 2
assert obj.choose(42) == 2
assert obj.chooseStatic(42) == 2
assert obj._chooseProtected(42) == 2
assert obj.unchanged(42) == 1


def test_fn_overloads():
Expand Down
Loading