Skip to content

Commit

Permalink
cpp_types: Add ability to map C++ types to Python to enable simple te…
Browse files Browse the repository at this point in the history
…mplating.
  • Loading branch information
EricCousineau-TRI committed Jan 13, 2018
1 parent 0109e99 commit 5928e0c
Show file tree
Hide file tree
Showing 10 changed files with 440 additions and 1 deletion.
2 changes: 1 addition & 1 deletion bindings/pydrake/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ drake_pybind_library(
cc_srcs = ["common_py.cc"],
package_info = PACKAGE_INFO,
py_deps = [
":util_py",
"//bindings/pydrake/util:module_shim_py",
],
py_srcs = [
"__init__.py",
Expand Down
34 changes: 34 additions & 0 deletions bindings/pydrake/util/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ load(
)
load(
"//tools/skylark:pybind.bzl",
"drake_pybind_cc_googletest",
"get_drake_pybind_installs",
"get_pybind_package_info",
)
Expand Down Expand Up @@ -55,6 +56,7 @@ drake_py_library(
PYBIND_LIBRARIES = []

PY_LIBRARIES = [
":cpp_types_py",
":module_py",
":module_shim_py",
]
Expand All @@ -66,6 +68,25 @@ drake_py_library(
deps = PYBIND_LIBRARIES + PY_LIBRARIES,
)

# ODR does not matter, because the singleton will be stored in Python.
drake_cc_library(
name = "cpp_types_pybind",
srcs = ["cpp_types_pybind.cc"],
hdrs = ["cpp_types_pybind.h"],
deps = [
":type_pack",
"@pybind11",
],
)

drake_py_library(
name = "cpp_types_py",
srcs = ["cpp_types.py"],
deps = [
":module_py",
],
)

install(
name = "install",
targets = PY_LIBRARIES,
Expand Down Expand Up @@ -100,4 +121,17 @@ drake_py_test(
],
)

drake_py_test(
name = "cpp_types_test",
deps = [
":cpp_types_py",
],
)

drake_pybind_cc_googletest(
name = "cpp_types_pybind_test",
cc_deps = [":cpp_types_pybind"],
py_deps = [":cpp_types_py"],
)

add_lint_tests()
84 changes: 84 additions & 0 deletions bindings/pydrake/util/cpp_types.py
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))
74 changes: 74 additions & 0 deletions bindings/pydrake/util/cpp_types_pybind.cc
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
56 changes: 56 additions & 0 deletions bindings/pydrake/util/cpp_types_pybind.h
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
74 changes: 74 additions & 0 deletions bindings/pydrake/util/test/cpp_types_pybind_test.cc
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);
}
Loading

0 comments on commit 5928e0c

Please sign in to comment.