Skip to content

Commit d1fe0e3

Browse files
committed
Implement C++ <-> Python type interop via JSON.
1 parent cfa553a commit d1fe0e3

File tree

10 files changed

+501
-16
lines changed

10 files changed

+501
-16
lines changed

Diff for: CMakeLists.txt

+13-5
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,7 @@ if(BTCPP_SQLITE_LOGGING)
136136
endif()
137137

138138
if(BTCPP_PYTHON)
139-
find_package(Python COMPONENTS Interpreter Development)
140-
find_package(pybind11 CONFIG)
141-
pybind11_add_module(btpy_cpp src/python_bindings.cpp)
142-
target_compile_options(btpy_cpp PRIVATE -Wno-gnu-zero-variadic-macro-arguments)
143-
target_link_libraries(btpy_cpp PRIVATE ${BTCPP_LIBRARY})
139+
list(APPEND BT_SOURCE src/python_types.cpp)
144140
endif()
145141

146142
######################################################
@@ -172,6 +168,18 @@ target_link_libraries(${BTCPP_LIBRARY}
172168
${BTCPP_EXTRA_LIBRARIES}
173169
)
174170

171+
if(BTCPP_PYTHON)
172+
find_package(Python COMPONENTS Interpreter Development)
173+
find_package(pybind11 CONFIG)
174+
175+
pybind11_add_module(btpy_cpp src/python_types.cpp src/python_bindings.cpp)
176+
target_compile_options(btpy_cpp PRIVATE -Wno-gnu-zero-variadic-macro-arguments)
177+
target_link_libraries(btpy_cpp PRIVATE ${BTCPP_LIBRARY})
178+
179+
target_link_libraries(${BTCPP_LIBRARY} PUBLIC Python::Python pybind11::pybind11)
180+
target_compile_definitions(${BTCPP_LIBRARY} PUBLIC BTCPP_PYTHON)
181+
endif()
182+
175183
target_include_directories(${BTCPP_LIBRARY}
176184
PUBLIC
177185
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>

Diff for: include/behaviortree_cpp/contrib/pybind11_json.hpp

+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/***************************************************************************
2+
* Copyright (c) 2019, Martin Renou *
3+
* *
4+
* Distributed under the terms of the BSD 3-Clause License. *
5+
* *
6+
* The full license is in the file LICENSE, distributed with this software. *
7+
****************************************************************************/
8+
9+
#ifndef PYBIND11_JSON_HPP
10+
#define PYBIND11_JSON_HPP
11+
12+
#include <string>
13+
#include <vector>
14+
15+
#include "behaviortree_cpp/contrib/json.hpp"
16+
17+
#include "pybind11/pybind11.h"
18+
19+
namespace pyjson
20+
{
21+
namespace py = pybind11;
22+
namespace nl = nlohmann;
23+
24+
inline py::object from_json(const nl::json& j)
25+
{
26+
if (j.is_null())
27+
{
28+
return py::none();
29+
}
30+
else if (j.is_boolean())
31+
{
32+
return py::bool_(j.get<bool>());
33+
}
34+
else if (j.is_number_unsigned())
35+
{
36+
return py::int_(j.get<nl::json::number_unsigned_t>());
37+
}
38+
else if (j.is_number_integer())
39+
{
40+
return py::int_(j.get<nl::json::number_integer_t>());
41+
}
42+
else if (j.is_number_float())
43+
{
44+
return py::float_(j.get<double>());
45+
}
46+
else if (j.is_string())
47+
{
48+
return py::str(j.get<std::string>());
49+
}
50+
else if (j.is_array())
51+
{
52+
py::list obj(j.size());
53+
for (std::size_t i = 0; i < j.size(); i++)
54+
{
55+
obj[i] = from_json(j[i]);
56+
}
57+
return obj;
58+
}
59+
else // Object
60+
{
61+
py::dict obj;
62+
for (nl::json::const_iterator it = j.cbegin(); it != j.cend(); ++it)
63+
{
64+
obj[py::str(it.key())] = from_json(it.value());
65+
}
66+
return obj;
67+
}
68+
}
69+
70+
inline nl::json to_json(const py::handle& obj)
71+
{
72+
if (obj.ptr() == nullptr || obj.is_none())
73+
{
74+
return nullptr;
75+
}
76+
if (py::isinstance<py::bool_>(obj))
77+
{
78+
return obj.cast<bool>();
79+
}
80+
if (py::isinstance<py::int_>(obj))
81+
{
82+
try
83+
{
84+
nl::json::number_integer_t s = obj.cast<nl::json::number_integer_t>();
85+
if (py::int_(s).equal(obj))
86+
{
87+
return s;
88+
}
89+
}
90+
catch (...)
91+
{
92+
}
93+
try
94+
{
95+
nl::json::number_unsigned_t u = obj.cast<nl::json::number_unsigned_t>();
96+
if (py::int_(u).equal(obj))
97+
{
98+
return u;
99+
}
100+
}
101+
catch (...)
102+
{
103+
}
104+
throw std::runtime_error("to_json received an integer out of range for both nl::json::number_integer_t and nl::json::number_unsigned_t type: " + py::repr(obj).cast<std::string>());
105+
}
106+
if (py::isinstance<py::float_>(obj))
107+
{
108+
return obj.cast<double>();
109+
}
110+
if (py::isinstance<py::bytes>(obj))
111+
{
112+
py::module base64 = py::module::import("base64");
113+
return base64.attr("b64encode")(obj).attr("decode")("utf-8").cast<std::string>();
114+
}
115+
if (py::isinstance<py::str>(obj))
116+
{
117+
return obj.cast<std::string>();
118+
}
119+
if (py::isinstance<py::tuple>(obj) || py::isinstance<py::list>(obj))
120+
{
121+
auto out = nl::json::array();
122+
for (const py::handle value : obj)
123+
{
124+
out.push_back(to_json(value));
125+
}
126+
return out;
127+
}
128+
if (py::isinstance<py::dict>(obj))
129+
{
130+
auto out = nl::json::object();
131+
for (const py::handle key : obj)
132+
{
133+
out[py::str(key).cast<std::string>()] = to_json(obj[key]);
134+
}
135+
return out;
136+
}
137+
throw std::runtime_error("to_json not implemented for this type of object: " + py::repr(obj).cast<std::string>());
138+
}
139+
}
140+
141+
// nlohmann_json serializers
142+
namespace nlohmann
143+
{
144+
namespace py = pybind11;
145+
146+
#define MAKE_NLJSON_SERIALIZER_DESERIALIZER(T) \
147+
template <> \
148+
struct adl_serializer<T> \
149+
{ \
150+
inline static void to_json(json& j, const T& obj) \
151+
{ \
152+
j = pyjson::to_json(obj); \
153+
} \
154+
\
155+
inline static T from_json(const json& j) \
156+
{ \
157+
return pyjson::from_json(j); \
158+
} \
159+
}
160+
161+
#define MAKE_NLJSON_SERIALIZER_ONLY(T) \
162+
template <> \
163+
struct adl_serializer<T> \
164+
{ \
165+
inline static void to_json(json& j, const T& obj) \
166+
{ \
167+
j = pyjson::to_json(obj); \
168+
} \
169+
}
170+
171+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::object);
172+
173+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::bool_);
174+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::int_);
175+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::float_);
176+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::str);
177+
178+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::list);
179+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::tuple);
180+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::dict);
181+
182+
MAKE_NLJSON_SERIALIZER_ONLY(py::handle);
183+
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::item_accessor);
184+
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::list_accessor);
185+
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::tuple_accessor);
186+
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::sequence_accessor);
187+
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::str_attr_accessor);
188+
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::obj_attr_accessor);
189+
190+
#undef MAKE_NLJSON_SERIALIZER
191+
#undef MAKE_NLJSON_SERIALIZER_ONLY
192+
}
193+
194+
// pybind11 caster
195+
namespace pybind11
196+
{
197+
namespace detail
198+
{
199+
template <> struct type_caster<nlohmann::json>
200+
{
201+
public:
202+
PYBIND11_TYPE_CASTER(nlohmann::json, _("json"));
203+
204+
bool load(handle src, bool)
205+
{
206+
try
207+
{
208+
value = pyjson::to_json(src);
209+
return true;
210+
}
211+
catch (...)
212+
{
213+
return false;
214+
}
215+
}
216+
217+
static handle cast(nlohmann::json src, return_value_policy /* policy */, handle /* parent */)
218+
{
219+
object obj = pyjson::from_json(src);
220+
return obj.release();
221+
}
222+
};
223+
}
224+
}
225+
226+
#endif

Diff for: include/behaviortree_cpp/json_export.h

+15-6
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,8 @@ namespace BT
2323
*/
2424

2525
class JsonExporter{
26-
27-
public:
28-
static JsonExporter& get() {
29-
static JsonExporter global_instance;
30-
return global_instance;
31-
}
26+
public:
27+
static JsonExporter& get();
3228

3329
/**
3430
* @brief toJson adds the content of "any" to the JSON "destination".
@@ -43,6 +39,19 @@ class JsonExporter{
4339
dst = val;
4440
}
4541

42+
template <typename T>
43+
T fromJson(const nlohmann::json& src) const
44+
{
45+
// TODO: Implement a similar "converter" interface as toJson.
46+
return src.template get<T>();
47+
}
48+
49+
template <typename T>
50+
void fromJson(const nlohmann::json& src, T& dst) const
51+
{
52+
dst = fromJson<T>(src);
53+
}
54+
4655
/// Register new JSON converters with addConverter<Foo>(),
4756
/// But works only if this function is implemented:
4857
///

Diff for: include/behaviortree_cpp/python_types.h

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#pragma once
2+
3+
#include <pybind11/pybind11.h>
4+
5+
#include "behaviortree_cpp/json_export.h"
6+
#include "behaviortree_cpp/contrib/json.hpp"
7+
#include "behaviortree_cpp/contrib/pybind11_json.hpp"
8+
#include "behaviortree_cpp/utils/safe_any.hpp"
9+
10+
namespace BT
11+
{
12+
13+
/**
14+
* @brief Generic method to convert Python objects to type T via JSON.
15+
*
16+
* For this function to succeed, the type T must be convertible from JSON via
17+
* the JsonExporter interface.
18+
*/
19+
template <typename T>
20+
bool fromPythonObject(const pybind11::object& obj, T& dest)
21+
{
22+
if constexpr (nlohmann::detail::is_getable<nlohmann::json, T>::value)
23+
{
24+
JsonExporter::get().fromJson<T>(obj, dest);
25+
return true;
26+
}
27+
28+
return false;
29+
}
30+
31+
/**
32+
* @brief Convert a BT::Any to a Python object via JSON.
33+
*
34+
* For this function to succeed, the type stored inside the Any must be
35+
* convertible to JSON via the JsonExporter interface.
36+
*/
37+
bool toPythonObject(const BT::Any& val, pybind11::object& dest);
38+
39+
} // namespace BT

Diff for: include/behaviortree_cpp/tree_node.h

+31-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@
2525
#include "behaviortree_cpp/utils/wakeup_signal.hpp"
2626
#include "behaviortree_cpp/scripting/script_parser.hpp"
2727

28+
#ifdef BTCPP_PYTHON
29+
#include <pybind11/pybind11.h>
30+
#include <pybind11/cast.h>
31+
32+
#include "behaviortree_cpp/python_types.h"
33+
#endif
34+
2835
#ifdef _MSC_VER
2936
#pragma warning(disable : 4127)
3037
#endif
@@ -442,11 +449,33 @@ inline Result TreeNode::getInput(const std::string& key, T& destination) const
442449
auto val = any_ref.get();
443450
if(!val->empty())
444451
{
445-
if (!std::is_same_v<T, std::string> &&
446-
val->type() == typeid(std::string))
452+
// Trivial conversion (T -> T)
453+
if (val->type() == typeid(T))
454+
{
455+
destination = val->cast<T>();
456+
}
457+
else if (!std::is_same_v<T, std::string> && val->type() == typeid(std::string))
447458
{
448459
destination = ParseString(val->cast<std::string>());
449460
}
461+
#ifdef BTCPP_PYTHON
462+
// py::object -> C++
463+
else if (val->type() == typeid(pybind11::object))
464+
{
465+
if (!fromPythonObject<T>(val->cast<pybind11::object>(), destination))
466+
{
467+
return nonstd::make_unexpected("Cannot convert from Python object");
468+
}
469+
}
470+
// C++ -> py::object
471+
else if constexpr (std::is_same_v<T, pybind11::object>)
472+
{
473+
if (!toPythonObject(*val, destination))
474+
{
475+
return nonstd::make_unexpected("Cannot convert to Python object");
476+
}
477+
}
478+
#endif
450479
else
451480
{
452481
destination = val->cast<T>();

0 commit comments

Comments
 (0)