Skip to content

Commit 7059bae

Browse files
committed
Support serialization metadata on template typedefs
1 parent 47d56b2 commit 7059bae

11 files changed

Lines changed: 168 additions & 9 deletions

File tree

DOCS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,19 @@ The python wrapper supports keyword arguments for functions/methods. Hence, the
168168
template<T, U> class Class2 { ... };
169169
typedef Class2<Type1, Type2> MyInstantiatedClass;
170170
```
171+
- Serialization can be enabled for one typedef without marking every
172+
specialization of the template serializable:
173+
174+
```cpp
175+
template<T> class Class3 { ... };
176+
@serializable
177+
typedef Class3<Type1> SerializableClass3;
178+
typedef Class3<Type2> PlainClass3;
179+
```
180+
181+
`@serializable` is wrapper metadata. It generates the same Python pickle
182+
and MATLAB save/load support as a `void serialize() const;` marker on a
183+
concrete wrapper class, but applies only to the annotated typedef.
171184
- Templates can also be defined for constructors, methods, properties and static methods.
172185
- In the class definition, appearances of the template argument(s) will be replaced with their
173186
instantiated types, e.g. `void setValue(const T& value);`.

gtwrap/interface_parser/annotations.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Pybind-specific annotations supported by wrapper interface files."""
1+
"""Annotations supported by wrapper interface files."""
22

33
from pyparsing import Regex
44

@@ -12,21 +12,36 @@
1212
def _reject_annotation(source, location, tokens):
1313
"""Raise a useful error for unknown or misplaced annotations."""
1414
annotation = tokens[0]
15+
context = "callable annotation"
1516
if annotation == "@pybind_lambda":
1617
message = (
1718
"annotation '@pybind_lambda' can only be applied to a method, "
1819
"static method, or global function"
1920
)
21+
hint = (
22+
"place '@pybind_lambda' after any template declaration and "
23+
"immediately before the callable declaration"
24+
)
25+
elif annotation == "@serializable":
26+
context = "typedef annotation"
27+
message = (
28+
"annotation '@serializable' can only be applied to a template "
29+
"typedef"
30+
)
31+
hint = "place '@serializable' immediately before the typedef declaration"
2032
else:
2133
message = f"malformed or unknown annotation '{annotation}'"
34+
hint = (
35+
"use a supported annotation such as '@pybind_lambda' or "
36+
"'@serializable' in its documented position"
37+
)
2238

2339
raise semantic_error(
2440
source,
2541
location,
26-
"callable annotation",
42+
context,
2743
message,
28-
"place '@pybind_lambda' after any template declaration and "
29-
"immediately before the callable declaration",
44+
hint,
3045
)
3146

3247

gtwrap/interface_parser/template.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212

1313
from typing import List
1414

15-
from pyparsing import Optional, ParseResults, DelimitedList # type: ignore
15+
from pyparsing import DelimitedList, Optional, ParseResults, Regex # type: ignore
1616

1717
from .tokens import (EQUAL, IDENT, LBRACE, LOPBRACK, RBRACE, ROPBRACK,
1818
SEMI_COLON, TEMPLATE, TYPEDEF)
1919
from .type import TemplatedType, Typename
2020

2121

22+
SERIALIZABLE = Regex(r"@serializable(?![A-Za-z0-9_])")
23+
24+
2225
class Template:
2326
"""
2427
Rule to parse templated values in the interface file.
@@ -83,17 +86,20 @@ class TypedefTemplateInstantiation:
8386
typedef SuperComplexName<Arg1, Arg2, Arg3> EasierName;
8487
```
8588
"""
86-
rule = (TYPEDEF + TemplatedType.rule("templated_type") +
89+
rule = (Optional(SERIALIZABLE("serializable")) +
90+
TYPEDEF + TemplatedType.rule("templated_type") +
8791
IDENT("new_name") +
8892
SEMI_COLON).set_parse_action(lambda t: TypedefTemplateInstantiation(
89-
t.templated_type[0], t.new_name))
93+
t.templated_type[0], t.new_name, bool(t.serializable)))
9094

9195
def __init__(self,
9296
templated_type: TemplatedType,
9397
new_name: str,
98+
serializable: bool = False,
9499
parent: str = ''):
95100
self.typename = templated_type.typename
96101
self.new_name = new_name
102+
self.serializable = serializable
97103
self.parent = parent
98104

99105
def __repr__(self):

gtwrap/template_instantiator/classes.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@ class InstantiatedClass(parser.Class):
1717
Instantiate the class defined in the interface file.
1818
"""
1919

20-
def __init__(self, original: parser.Class, instantiations=(), new_name=''):
20+
def __init__(self,
21+
original: parser.Class,
22+
instantiations=(),
23+
new_name='',
24+
serializable=False):
2125
"""
2226
Template <T, U>
2327
Instantiations: [T1, U1]
2428
"""
2529
self.original = original
2630
self.instantiations = instantiations
31+
self.serializable = serializable
2732

2833
self.template = None
2934
self.is_virtual = original.is_virtual
@@ -58,6 +63,12 @@ def __init__(self, original: parser.Class, instantiations=(), new_name=''):
5863

5964
# Instantiate all instance methods
6065
self.methods = self.instantiate_methods(typenames)
66+
if serializable and not any(
67+
method.name in ('serialize', 'serializable')
68+
for method in self.methods):
69+
self.methods.append(
70+
parser.Method.rule.parse_string(
71+
"void serialize() const;")[0])
6172

6273
self.dunder_methods = original.dunder_methods
6374

gtwrap/template_instantiator/namespace.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ def instantiate_namespace(namespace):
6464
typedef_content.append(
6565
InstantiatedClass(original_element,
6666
typedef_inst.typename.instantiations,
67-
typedef_inst.new_name))
67+
typedef_inst.new_name,
68+
typedef_inst.serializable))
6869
elif isinstance(original_element, parser.GlobalFunction):
6970
typedef_content.append(
7071
InstantiatedGlobalFunction(
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace gtsam {
2+
3+
template<T>
4+
class SerializableTypedefFixture {
5+
SerializableTypedefFixture();
6+
};
7+
8+
@serializable
9+
typedef gtsam::SerializableTypedefFixture<int> SerializableFixture;
10+
typedef gtsam::SerializableTypedefFixture<double> PlainFixture;
11+
12+
} // namespace gtsam

tests/test_interface_parser.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,14 @@ def test_typedef_template_instantiation(self):
408408
self.assertEqual("BearingFactor", typedef.typename.name)
409409
self.assertEqual(["gtsam"], typedef.typename.namespaces)
410410
self.assertEqual(3, len(typedef.typename.instantiations))
411+
self.assertFalse(typedef.serializable)
412+
413+
serializable = TypedefTemplateInstantiation.rule.parse_string("""
414+
@serializable
415+
typedef gtsam::BearingFactor<gtsam::Pose2, gtsam::Point2,
416+
gtsam::Rot2> SerializableBearingFactor2D;
417+
""")[0]
418+
self.assertTrue(serializable.serializable)
411419

412420
def test_base_class(self):
413421
"""Test a base class."""

tests/test_matlab_wrapper.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,36 @@ def test_geometry(self):
8282
actual = osp.join(self.MATLAB_ACTUAL_DIR, file)
8383
self.compare_and_diff(file, actual)
8484

85+
def test_serializable_template_typedef(self):
86+
"""Serialization metadata applies to one MATLAB typedef only."""
87+
source = osp.join(self.INTERFACE_DIR, 'serializable_typedef.i')
88+
wrapper = MatlabWrapper(module_name='serializable_typedef',
89+
top_module_namespace=['gtsam'],
90+
ignore_classes=[''],
91+
use_boost_serialization=True)
92+
wrapper.wrap([source], path=self.MATLAB_ACTUAL_DIR)
93+
94+
with open(osp.join(self.MATLAB_ACTUAL_DIR, '+gtsam',
95+
'SerializableFixture.m'),
96+
'r', encoding='UTF-8') as generated:
97+
serializable = generated.read()
98+
with open(osp.join(self.MATLAB_ACTUAL_DIR, '+gtsam', 'PlainFixture.m'),
99+
'r', encoding='UTF-8') as generated:
100+
plain = generated.read()
101+
with open(osp.join(self.MATLAB_ACTUAL_DIR,
102+
'serializable_typedef_wrapper.cpp'),
103+
'r', encoding='UTF-8') as generated:
104+
cpp = generated.read()
105+
106+
self.assertIn('string_serialize', serializable)
107+
self.assertIn('string_deserialize', serializable)
108+
self.assertNotIn('string_serialize', plain)
109+
self.assertNotIn('string_deserialize', plain)
110+
self.assertIn(
111+
'BOOST_CLASS_EXPORT_GUID(SerializableFixture, '
112+
'"gtsamSerializableFixture")', cpp)
113+
self.assertNotIn('BOOST_CLASS_EXPORT_GUID(PlainFixture', cpp)
114+
85115
def test_matrix_view_arguments(self):
86116
"""Test that matrix view arguments use MATLAB double arrays directly."""
87117
file = osp.join(self.INTERFACE_DIR, 'matrix_views.i')

tests/test_parser_diagnostics.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@ def test_misplaced_callable_annotation(self):
124124
)
125125
self.assertIn("immediately before the callable", error.hint)
126126

127+
def test_misplaced_serializable_annotation(self):
128+
"""The serialization annotation applies only to template typedefs."""
129+
error = self.assert_parse_error(
130+
"@serializable class Foo {};",
131+
line=1,
132+
column=1,
133+
context="typedef annotation",
134+
expected=(
135+
"annotation '@serializable' can only be applied to a template "
136+
"typedef"
137+
),
138+
)
139+
self.assertIn("immediately before the typedef", error.hint)
140+
127141
def test_misplaced_annotation_after_template(self):
128142
self.assert_parse_error(
129143
"class Foo { template<T> @pybind_lambda Foo(T value); };",

tests/test_pybind_wrapper.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,26 @@ def test_geometry(self):
114114

115115
self.compare_and_diff('geometry_pybind.cpp', output)
116116

117+
def test_serializable_template_typedef(self):
118+
"""Serialization metadata applies to one template typedef only."""
119+
source = osp.join(self.INTERFACE_DIR, 'serializable_typedef.i')
120+
output = self.wrap_content([source],
121+
'serializable_typedef_py',
122+
self.PYTHON_ACTUAL_DIR,
123+
use_boost_serialization=True)
124+
with open(output, 'r', encoding='UTF-8') as generated:
125+
content = generated.read()
126+
127+
self.assertEqual(1, content.count('.def("serialize"'))
128+
self.assertEqual(1, content.count('.def("deserialize"'))
129+
self.assertEqual(1, content.count('.def(py::pickle('))
130+
self.assertIn(
131+
'BOOST_CLASS_EXPORT(gtsam::SerializableTypedefFixture<int>)',
132+
content)
133+
self.assertNotIn(
134+
'BOOST_CLASS_EXPORT(gtsam::SerializableTypedefFixture<double>)',
135+
content)
136+
117137
def test_functions(self):
118138
"""Test interface file with function info."""
119139
source = osp.join(self.INTERFACE_DIR, 'functions.i')

0 commit comments

Comments
 (0)