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
81 changes: 81 additions & 0 deletions .agents/skills/annotate-pybind-adapters/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
name: annotate-pybind-adapters
description: Diagnose generated pybind compilation failures caused by wrapper interface signatures that differ from their C++ declarations, then apply the wrap `@pybind_lambda` annotation to only the affected methods, static methods, or global functions. Use for compile-annotate-regenerate migrations with full-signature callable-pointer casts, especially for omitted parameters, value/reference differences, synthetic container accessors, incompatible types, inheritance, or templates.
---

# Annotate Pybind Adapters

Use `@pybind_lambda` as an explicit escape hatch when a wrapper declaration is
an adapter rather than the exact C++ callable signature. Keep full-signature
callable-pointer casts as the default.

Read the repository instructions and the `Pybind Callable Adapters` section in
`DOCS.md` before editing an interface file.

## Workflow

1. Reproduce the generated pybind compilation failure with the narrowest
available build target.
2. Locate the failing binding in generated C++ and map it back to one callable
declaration in the wrapper `.i` file.
3. Inspect the real C++ header. Do not infer signature equivalence from the `.i`
file or from generator heuristics.
4. Confirm that the old forwarding call is valid even though the generated
full-signature `static_cast` does not match the C++ declaration.
5. Add the marker after any `template<...>` prefix and immediately before that
callable:

```text
@pybind_lambda
ReturnType method(Arguments...);

template<T = {double}>
@pybind_lambda
T templatedMethod(T value);
```

6. Regenerate the binding and verify the annotated declaration emits a lambda
while nearby unannotated declarations still emit full-signature casts.
7. Re-run the failing compile target, relevant wrapper tests, and the full test
suite prescribed by the repository.
8. Report each annotation and the concrete C++ signature mismatch that requires
it.

Use the `py312` conda environment for Python commands in this workspace. In the
standalone wrap repository, run focused pytest tests and then:

```bash
conda run -n py312 python -m pytest tests
```

When working in an integrated build that provides the repository-prescribed
target, run its `make -j6 testXXX.run` target with the required permissions.

## Decision Rules

Annotate when the wrapper intentionally differs from C++, including:

- omitted underlying parameters with C++ defaults;
- wrapper values that bind to C++ references;
- synthetic value-returning interfaces such as `at` or `front` over reference
returns;
- incompatible types or inherited/template declarations whose pointer signature
does not match the wrapper spelling.

Do not annotate merely because a callable is overloaded or templated. A
full-signature cast selects an exact overload even when other overloads appear
only in the C++ header. The generator also retains its existing automatic
template-specialization and adapter cases.

## Guardrails

- Annotate one callable declaration at a time; never annotate a class or file in
bulk.
- Do not add symbol-name lists, GTSAM-specific heuristics, or guessed mismatch
detection to wrap.
- Do not change declaration/binding order or type-registration behavior while
fixing an adapter compile failure.
- Preserve Python names, defaults, policies, docstrings, and overload exposure.
- Treat the marker as pybind-only. MATLAB output must remain unchanged.
- Remove no existing automatic lambdas unless the task explicitly changes their
semantics.
4 changes: 4 additions & 0 deletions .agents/skills/annotate-pybind-adapters/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Annotate Pybind Adapters"
short_description: "Annotate mismatched pybind callables safely"
default_prompt: "Use $annotate-pybind-adapters to diagnose this generated pybind compile failure and annotate only the mismatched callable."
88 changes: 88 additions & 0 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ The python wrapper supports keyword arguments for functions/methods. Hence, the
```cpp
template<T, R, S>
```

- Global variables
- Similar to global functions, the wrapper supports global variables as well.
- Currently we only support primitive types, such as `double`, `int`, `string`, etc.
Expand Down Expand Up @@ -225,6 +226,93 @@ The python wrapper supports keyword arguments for functions/methods. Hence, the
- Unfortunately, this means that aliases can no longer be used.
- Similarly, there can be multiple `preamble.h` and `specializations.h` files. Each of these should match the module file name.

## Pybind Callable Adapters

The Python generator uses explicit full-signature C++ callable-pointer casts for
ordinary wrapper declarations. For example:

```cpp
class Example {
int size() const;
static Example Create();
};

int globalFunction(int value);
```

generates bindings equivalent to:

```cpp
static_cast<int (Example::*)() const>(&Example::size)
static_cast<Example (*)()>(&Example::Create)
static_cast<int (*)(int)>(&::globalFunction)
```

The return type, argument types, class type, and member constness are always
part of the cast. This selects an exact overload even when other overloads exist
in the C++ header but are not listed in the `.i` file.

A wrapper `.i` declaration does not always reproduce the underlying C++
signature exactly. Add `@pybind_lambda` to one method, static method, or global
function when its wrapper signature is intentionally an adapter:

```cpp
class Example {
@pybind_lambda
int lookup(int index) const;

@pybind_lambda
static Example Load(string filename);

template<T = {double}>
@pybind_lambda
T convert(T value) const;
};

@pybind_lambda
int globalFunction(int value);
```

Place the annotation after any `template<...>` declaration and immediately
before the callable. The annotated instance method is emitted using the existing
forwarding-lambda path, for example:

```cpp
.def("lookup",
[](Example* self, int index) {
return self->lookup(index);
},
py::arg("index"))
```

The annotation is useful when the interface intentionally:

- omits underlying C++ parameters that have defaults;
- accepts values where the C++ function accepts references;
- synthesizes a value-returning container operation such as `at` or `front`;
- uses types that are not equivalent to the callable's declared signature; or
- otherwise adapts an inherited or templated member's signature.

Do not annotate a callable merely because it is overloaded, including when an
overload exists only in the C++ header. An unannotated `.i` declaration whose
complete function type matches the desired C++ overload uses the generated
full-signature cast. Likewise, existing automatic lambdas for template
specializations, renamed bindings, print/repr, serialization, and synthesized
dunder methods do not need this marker.

`@pybind_lambda` affects only pybind output. MATLAB generation ignores the
stored marker. Template instantiation preserves the marker on every instantiated
callable. Python-visible names, keyword escaping, default arguments, argument
policies, return-value policies such as `reference_internal`, and generated
docstrings are appended exactly as they are for the normal binding path.

Wrap does not inspect the C++ AST and cannot determine whether an interface
signature exactly matches the real declaration. Use the annotation only after
comparing the `.i` declaration with the C++ header or after diagnosing a
generated-code compilation error. Unknown annotations and annotations on
constructors, properties, classes, enums, or other unsupported declarations are
parser errors.

### TODO
- Handle `gtsam::Rot3M` conversions to quaternions.
- Parse return of const ref arguments.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ For more information, please follow our [tutorial](https://github.com/borglab/gt

## Documentation

Documentation for wrapping C++ code can be found [here](https://github.com/borglab/wrap/blob/master/DOCS.md).
Documentation for wrapping C++ code can be found [here](https://github.com/borglab/wrap/blob/master/DOCS.md), including the [pybind callable-adapter annotation](https://github.com/borglab/wrap/blob/master/DOCS.md#pybind-callable-adapters).

## Python Wrapper

Expand Down
36 changes: 36 additions & 0 deletions gtwrap/interface_parser/annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Pybind-specific annotations supported by wrapper interface files."""

from pyparsing import Regex

from .diagnostics import semantic_error
from .template import Template


PYBIND_LAMBDA = Regex(r"@pybind_lambda(?![A-Za-z0-9_])")


def _reject_annotation(source, location, tokens):
"""Raise a useful error for unknown or misplaced annotations."""
annotation = tokens[0]
if annotation == "@pybind_lambda":
message = (
"annotation '@pybind_lambda' can only be applied to a method, "
"static method, or global function"
)
else:
message = f"malformed or unknown annotation '{annotation}'"

raise semantic_error(
source,
location,
"callable annotation",
message,
"place '@pybind_lambda' after any template declaration and "
"immediately before the callable declaration",
)


UNSUPPORTED_ANNOTATION = Regex(r"@[^\s;{}()]+|@").set_parse_action(
_reject_annotation)
UNSUPPORTED_TEMPLATED_ANNOTATION = (
Template.rule.suppress() + UNSUPPORTED_ANNOTATION)
33 changes: 27 additions & 6 deletions gtwrap/interface_parser/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from pyparsing import ZeroOrMore # type: ignore
from pyparsing import Literal, Optional, Word, alphas

from .annotations import (PYBIND_LAMBDA, UNSUPPORTED_ANNOTATION,
UNSUPPORTED_TEMPLATED_ANNOTATION)
from .enum import Enum
from .function import ArgumentList, ReturnType
from .template import Template
Expand All @@ -39,28 +41,37 @@ class Hello {
"""
rule = (
Optional(Template.rule("template")) #
+ Optional(PYBIND_LAMBDA("pybind_lambda")) #
+ ReturnType.rule("return_type") #
+ IDENT("name") #
+ LPAREN #
+ ArgumentList.rule("args_list") #
+ RPAREN #
+ Optional(CONST("is_const")) #
+ SEMI_COLON # BR
).set_parse_action(lambda t: Method(t.template, t.name, t.return_type, t.
args_list, t.is_const))
).set_parse_action(lambda t: Method(
t.template,
t.name,
t.return_type,
t.args_list,
t.is_const,
force_pybind_lambda=bool(t.pybind_lambda),
))

def __init__(self,
template: Union[Template, Any],
name: str,
return_type: ReturnType,
args: ArgumentList,
is_const: str,
parent: Union["Class", Any] = ''):
parent: Union["Class", Any] = '',
force_pybind_lambda: bool = False):
self.template = template
self.name = name
self.return_type = return_type
self.args = args
self.is_const = is_const
self.force_pybind_lambda = force_pybind_lambda

self.parent = parent

Expand Down Expand Up @@ -91,26 +102,34 @@ class Hello {
"""
rule = (
Optional(Template.rule("template")) #
+ Optional(PYBIND_LAMBDA("pybind_lambda")) #
+ STATIC #
+ ReturnType.rule("return_type") #
+ IDENT("name") #
+ LPAREN #
+ ArgumentList.rule("args_list") #
+ RPAREN #
+ SEMI_COLON # BR
).set_parse_action(
lambda t: StaticMethod(t.name, t.return_type, t.args_list, t.template))
).set_parse_action(lambda t: StaticMethod(
t.name,
t.return_type,
t.args_list,
t.template,
force_pybind_lambda=bool(t.pybind_lambda),
))

def __init__(self,
name: str,
return_type: ReturnType,
args: ArgumentList,
template: Union[Template, Any] = None,
parent: Union["Class", Any] = ''):
parent: Union["Class", Any] = '',
force_pybind_lambda: bool = False):
self.name = name
self.return_type = return_type
self.args = args
self.template = template
self.force_pybind_lambda = force_pybind_lambda

self.parent = parent

Expand Down Expand Up @@ -291,6 +310,8 @@ class Members:
^ Variable.rule #
^ Operator.rule #
^ Enum.rule #
^ UNSUPPORTED_TEMPLATED_ANNOTATION #
^ UNSUPPORTED_ANNOTATION #
).set_parse_action(lambda t: Class.Members(t.as_list()))

def __init__(self, members: List[Union[Constructor, Method,
Expand Down
18 changes: 14 additions & 4 deletions gtwrap/interface_parser/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from pyparsing import Literal, Optional, ParseResults, DelimitedList

from .annotations import PYBIND_LAMBDA
from .template import Template
from .tokens import (COMMA, DEFAULT_ARG, EQUAL, IDENT, LOPBRACK, LPAREN, PAIR,
ROPBRACK, RPAREN, SEMI_COLON)
Expand Down Expand Up @@ -156,25 +157,34 @@ class GlobalFunction:
Rule to parse functions defined in the global scope.
"""
rule = (
Optional(Template.rule("template")) + ReturnType.rule("return_type") #
Optional(Template.rule("template")) #
+ Optional(PYBIND_LAMBDA("pybind_lambda")) #
+ ReturnType.rule("return_type") #
+ IDENT("name") #
+ LPAREN #
+ ArgumentList.rule("args_list") #
+ RPAREN #
+ SEMI_COLON #
).set_parse_action(lambda t: GlobalFunction(t.name, t.return_type, t.
args_list, t.template))
).set_parse_action(lambda t: GlobalFunction(
t.name,
t.return_type,
t.args_list,
t.template,
force_pybind_lambda=bool(t.pybind_lambda),
))

def __init__(self,
name: str,
return_type: ReturnType,
args_list: ArgumentList,
template: Template,
parent: Any = ''):
parent: Any = '',
force_pybind_lambda: bool = False):
self.name = name
self.return_type = return_type
self.args = args_list
self.template = template
self.force_pybind_lambda = force_pybind_lambda

self.parent = parent
self.return_type.parent = self
Expand Down
4 changes: 4 additions & 0 deletions gtwrap/interface_parser/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from pyparsing import (ParseBaseException, ZeroOrMore, cpp_style_comment, # type: ignore
string_end)

from .annotations import (UNSUPPORTED_ANNOTATION,
UNSUPPORTED_TEMPLATED_ANNOTATION)
from .classes import Class
from .declaration import ForwardDeclaration, Include
from .enum import Enum
Expand Down Expand Up @@ -45,6 +47,8 @@ class Module:
^ Enum.rule #
^ Variable.rule #
^ Namespace.rule #
^ UNSUPPORTED_TEMPLATED_ANNOTATION #
^ UNSUPPORTED_ANNOTATION #
).set_parse_action(lambda t: Namespace('', t.as_list())) +
string_end)

Expand Down
Loading
Loading