-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cpp_types: Add ability to map C++ types to Python to enable simple te…
…mplating.
- Loading branch information
1 parent
0109e99
commit 5928e0c
Showing
10 changed files
with
440 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
from __future__ import absolute_import, print_function | ||
|
||
import ctypes | ||
import numpy as np | ||
|
||
""" | ||
@file | ||
Defines a mapping between Python and alias types, and provides canonical Python | ||
types as they relate to C++. | ||
""" | ||
|
||
|
||
def _get_type_name(t): | ||
# Gets type name as a string. | ||
if t.__module__ != "__builtin__": | ||
return t.__module__ + "." + t.__name__ | ||
else: | ||
return t.__name__ | ||
|
||
|
||
class _StrictMap(object): | ||
# Provides a map which may only add a key once. | ||
def __init__(self): | ||
self._values = dict() | ||
|
||
def add(self, key, value): | ||
assert key not in self._values, "Already added: {}".format(key) | ||
self._values[key] = value | ||
|
||
def get(self, key, default): | ||
return self._values.get(key, default) | ||
|
||
|
||
class _AliasRegistry(object): | ||
# Registers aliases for a set of objects. This will be used for template | ||
# parameters. | ||
def __init__(self): | ||
self._to_canonical = _StrictMap() | ||
self._register_common() | ||
|
||
def _register_common(self): | ||
# Register common Python aliases relevant for C++. | ||
self.register(float, ctypes.c_double, np.double) | ||
self.register(np.float32, ctypes.c_float) | ||
self.register(int, np.int32, ctypes.c_int32) | ||
self.register(np.uint32, ctypes.c_uint32) | ||
|
||
def register(self, canonical, *aliases): | ||
# Registers a set of aliases to a canonical value. | ||
for alias in aliases: | ||
self._to_canonical.add(alias, canonical) | ||
|
||
def get_canonical(self, alias, default_same=True): | ||
# Gets registered canonical type if there is a mapping; otherwise | ||
# return default (same if `default_same`, or None otherwise). | ||
default = alias | ||
if not default_same: | ||
default = None | ||
return self._to_canonical.get(alias, default) | ||
|
||
def get_name(self, alias): | ||
# Gets string for an alias. | ||
canonical = self.get_canonical(alias) | ||
if isinstance(canonical, type): | ||
return _get_type_name(canonical) | ||
else: | ||
# For literals. | ||
return str(canonical) | ||
|
||
|
||
# Create singleton instance. | ||
_aliases = _AliasRegistry() | ||
|
||
|
||
def get_types_canonical(param): | ||
"""Gets the canonical types for a set of Python types (canonical as in | ||
how they relate to C++ types. """ | ||
return tuple(map(_aliases.get_canonical, param)) | ||
|
||
|
||
def get_type_names(param): | ||
"""Gets the canonical type names for a set of Python types (canonical as in | ||
how they relate to C++ types. """ | ||
return tuple(map(_aliases.get_name, param)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
#include "drake/bindings/pydrake/util/cpp_types_pybind.h" | ||
|
||
#include <pybind11/eval.h> | ||
|
||
namespace drake { | ||
namespace pydrake { | ||
namespace internal { | ||
namespace { | ||
|
||
// Creates a Python object that should uniquely hash for a primitive C++ | ||
// type. | ||
py::object GetPyHash(const std::type_info& tinfo) { | ||
return py::make_tuple("cpp_type", tinfo.hash_code()); | ||
} | ||
|
||
// Registers C++ type. | ||
template <typename T> | ||
void RegisterType( | ||
py::module m, py::object aliases, const std::string& canonical_str) { | ||
// Create an object that is a unique hash. | ||
py::object canonical = py::eval(canonical_str, m.attr("__dict__")); | ||
py::object alias = GetPyHash(typeid(T)); | ||
aliases.attr("register")(canonical, alias); | ||
} | ||
|
||
// Registers common C++ types. | ||
void RegisterCommon(py::module m, py::object aliases) { | ||
// Make mappings for C++ RTTI to Python types. | ||
// Unfortunately, this is hard to obtain from `pybind11`. | ||
RegisterType<bool>(m, aliases, "bool"); | ||
RegisterType<std::string>(m, aliases, "str"); | ||
RegisterType<double>(m, aliases, "float"); | ||
RegisterType<float>(m, aliases, "np.float32"); | ||
RegisterType<int>(m, aliases, "int"); | ||
RegisterType<uint32_t>(m, aliases, "np.uint32"); | ||
RegisterType<int64_t>(m, aliases, "np.int64"); | ||
// For supporting generic Python types. | ||
RegisterType<py::object>(m, aliases, "object"); | ||
} | ||
|
||
} // namespace | ||
|
||
py::object GetTypeAliases() { | ||
py::module m = py::module::import("pydrake.util.cpp_types"); | ||
py::object aliases = m.attr("_aliases"); | ||
const char registered_check[] = "_register_common_cpp"; | ||
if (!py::hasattr(m, registered_check)) { | ||
RegisterCommon(m, aliases); | ||
m.attr(registered_check) = true; | ||
} | ||
return aliases; | ||
} | ||
|
||
py::object GetPyTypeImpl(const std::type_info& tinfo) { | ||
py::object canonical = | ||
GetTypeAliases().attr("get_canonical")(GetPyHash(tinfo), false); | ||
if (!canonical.is_none()) { | ||
return canonical; | ||
} else { | ||
// If this C++ type was not explicitly registered above, then attempt | ||
// to get the pybind, erroring out if it's not registered. | ||
// WARNING: Internal API :( | ||
auto* info = py::detail::get_type_info(tinfo); | ||
if (!info) { | ||
throw std::runtime_error("Unknown custom type"); | ||
} | ||
py::handle h(reinterpret_cast<PyObject*>(info->type)); | ||
return py::reinterpret_borrow<py::object>(h); | ||
} | ||
} | ||
|
||
} // namespace internal | ||
} // namespace pydrake | ||
} // namespace drake |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
#pragma once | ||
|
||
/// @file | ||
/// Provides a mechanism to map C++ types to canonical Python types. | ||
|
||
#include <string> | ||
#include <typeinfo> | ||
#include <vector> | ||
|
||
#include <pybind11/pybind11.h> | ||
|
||
#include "drake/bindings/pydrake/util/type_pack.h" | ||
|
||
namespace py = pybind11; | ||
|
||
namespace drake { | ||
namespace pydrake { | ||
namespace internal { | ||
|
||
// Gets singleton for type aliases from `cpp_types`. | ||
py::object GetTypeAliases(); | ||
|
||
// Gets Python type object given `std::type_info`. | ||
py::object GetPyTypeImpl(const std::type_info& tinfo); | ||
|
||
// Gets Python type for a C++ type (base case). | ||
template <typename T> | ||
inline py::object GetPyTypeImpl(type_pack<T> = {}) { | ||
return GetPyTypeImpl(typeid(T)); | ||
} | ||
|
||
// Gets Python literal for a C++ literal (specialization). | ||
template <typename T, T Value> | ||
inline py::object GetPyTypeImpl( | ||
type_pack<std::integral_constant<T, Value>> = {}) { | ||
return py::cast(Value); | ||
} | ||
|
||
} // namespace internal | ||
|
||
/// Gets the canonical Python type for a given C++ type. | ||
template <typename T> | ||
inline py::object GetPyType(type_pack<T> tag = {}) { | ||
// Explicitly provide `tag` so that inference can handle the different | ||
// cases. | ||
return internal::GetPyTypeImpl(tag); | ||
} | ||
|
||
/// Gets the canonical Python types for each C++ type. | ||
template <typename ... Ts> | ||
inline py::tuple GetPyTypes(type_pack<Ts...> = {}) { | ||
return py::make_tuple(GetPyType<Ts>()...); | ||
} | ||
|
||
} // namespace pydrake | ||
} // namespace drake |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
#include "drake/bindings/pydrake/util/cpp_types_pybind.h" | ||
|
||
// @file | ||
// Tests the public interfaces in `cpp_types.py` and `cpp_types_pybind.h`. | ||
|
||
#include <string> | ||
|
||
#include <gtest/gtest.h> | ||
#include <pybind11/embed.h> | ||
#include <pybind11/eval.h> | ||
#include <pybind11/pybind11.h> | ||
|
||
using std::string; | ||
|
||
namespace drake { | ||
namespace pydrake { | ||
|
||
bool PyEquals(py::object lhs, py::object rhs) { | ||
return lhs.attr("__eq__")(rhs).cast<bool>(); | ||
} | ||
|
||
template <typename T> | ||
bool CheckPyType(const string& py_expr_expected, type_pack<T> = {}) { | ||
py::object actual = GetPyType<T>(); | ||
py::object expected = py::eval(py_expr_expected.c_str()); | ||
return actual.is(expected); | ||
} | ||
|
||
template <typename T, T Value> | ||
using constant = std::integral_constant<T, Value>; | ||
|
||
struct CustomCppType {}; | ||
|
||
GTEST_TEST(CppTypesTest, InCpp) { | ||
// Define custom class only once here. | ||
py::module m("__main__"); | ||
py::globals()["np"] = py::module::import("numpy"); | ||
|
||
py::class_<CustomCppType>(m, "CustomCppType"); | ||
|
||
// Check C++ behavior. | ||
ASSERT_TRUE(CheckPyType<bool>("bool")); | ||
ASSERT_TRUE(CheckPyType<std::string>("str")); | ||
ASSERT_TRUE(CheckPyType<double>("float")); | ||
ASSERT_TRUE(CheckPyType<float>("np.float32")); | ||
ASSERT_TRUE(CheckPyType<int>("int")); | ||
ASSERT_TRUE(CheckPyType<py::object>("object")); | ||
|
||
// Custom types. | ||
ASSERT_TRUE(CheckPyType<CustomCppType>("CustomCppType")); | ||
|
||
// Literals parameters. | ||
ASSERT_TRUE(CheckPyType<std::true_type>("True")); | ||
ASSERT_TRUE((CheckPyType<constant<int, -1>>("-1"))); | ||
ASSERT_TRUE((CheckPyType<constant<uint, 1>>("1"))); | ||
|
||
// Packs / tuples. | ||
ASSERT_TRUE(PyEquals(GetPyTypes<int, bool>(), py::eval("int, bool"))); | ||
} | ||
|
||
int main(int argc, char** argv) { | ||
// Reconstructing `scoped_interpreter` multiple times (e.g. via `SetUp()`) | ||
// while *also* importing `numpy` wreaks havoc. | ||
py::scoped_interpreter guard; | ||
::testing::InitGoogleTest(&argc, argv); | ||
return RUN_ALL_TESTS(); | ||
} | ||
|
||
} // namespace pydrake | ||
} // namespace drake | ||
|
||
int main(int argc, char** argv) { | ||
return drake::pydrake::main(argc, argv); | ||
} |
Oops, something went wrong.