RosettaCommons / binder

Binder, tool for automatic generation of Python bindings

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Binder skips some methods

mattangus opened this issue · comments

Thanks for putting this all together!

I am seeing some methods of a class being skipped. Binder doesn't seem to tell me why. Here is a minimal example:

.
├── build/
├── CMakeLists.txt
├── config.cfg
├── generated/
├── pybind11/
└── src/
    ├── all_includes.hpp
    └── TestClass.hpp

CMakeLists.txt

find_package (Eigen3 REQUIRED)
add_subdirectory(pybind11)

set(MODULE_NAME test_module)

execute_process(
    COMMAND bash "-c" "rm ${CMAKE_CURRENT_SOURCE_DIR}/generated/* -r"
)

execute_process(
    COMMAND bash "-c" "binder --root-module ${MODULE_NAME} --prefix ${CMAKE_CURRENT_SOURCE_DIR}/generated ${CMAKE_CURRENT_SOURCE_DIR}/src/all_includes.hpp --config ${CMAKE_CURRENT_SOURCE_DIR}/config.cfg -- -std=c++17 -I${CMAKE_CURRENT_SOURCE_DIR}/src -I${EIGEN3_INCLUDE_DIR}"
    RESULT_VARIABLE RET_CODE
)
if(RET_CODE AND NOT RET_CODE EQUAL 0)
    message( FATAL_ERROR "binder failed to generate files. See above errors")
endif()

file(GLOB_RECURSE SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/generated/*.cpp)

pybind11_add_module(${MODULE_NAME} ${SRC_FILES})

target_include_directories(
    ${MODULE_NAME}
    PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/src
    ${EIGEN3_INCLUDE_DIR}
)

config.cfg

+include <pybind11/stl.h>
+include <pybind11/eigen.h>

+namespace test

TestClass.hpp

#pragma once

#include <Eigen/Dense>

namespace test {

  template <typename T>
  struct TestClass
  {
    //! @brief Types.
    using scalar_type = T;
    using vector2_type = Eigen::Matrix<T, 2, 1>;
    using vector3_type = Eigen::Matrix<T, 3, 1>;
    using matrix3_type = Eigen::Matrix<T, 3, 3>;

    matrix3_type amat;
    inline auto multiply(const vector2_type& x) const -> vector3_type
    {
      return amat * x.homogeneous();
    }

    inline auto scale(T factor) -> void
    {
      amat.block(0, 0, 2, 3) /= factor;
    }
  };

  // is there a better way to do this?
  class TestClassf : public TestClass<float> {};

}  // namespace test

all_includes.hpp

#include <TestClass.hpp>

Running cmake .. from build generates the following code:

void bind_TestClass(std::function< pybind11::module &(std::string const &namespace_) > &M)
{
	{ // test::TestClass file:TestClass.hpp line:9
		pybind11::class_<test::TestClass<float>, std::shared_ptr<test::TestClass<float>>> cl(M("test"), "TestClass_float_t", "");
		cl.def( pybind11::init( [](){ return new test::TestClass<float>(); } ) );
		cl.def( pybind11::init( [](test::TestClass<float> const &o){ return new test::TestClass<float>(o); } ) );
		cl.def_readwrite("amat", &test::TestClass<float>::amat);
		cl.def("scale", (void (test::TestClass<float>::*)(float)) &test::TestClass<float>::scale, "C++: test::TestClass<float>::scale(float) --> void", pybind11::arg("factor"));
	}
	{ // test::TestClassf file:TestClass.hpp line:30
		pybind11::class_<test::TestClassf, std::shared_ptr<test::TestClassf>, test::TestClass<float>> cl(M("test"), "TestClassf", "");
		cl.def( pybind11::init( [](){ return new test::TestClassf(); } ) );
	}
}

As you can see the multiply method is not included in the generated code. Presumably it's something to do with Eigen. Any idea why?

It would be nice to have more levels of verbosity (or a --explain arg). Ideally this kind of skipping would be explained by binder (unless this is a bug).

You can also download the above files here: test_binder.zip.

After mucking about with binders source I've discovered that the return type is not bindable Eigen::Matrix has isDependentType() == true. So the return type and parameters are not bindable. Is this a bug? amat is bound with no problem.

Actually scratch that. It's becuase Eigen::Matrix has getPointOfInstantiation().isInvalid() == false. Not quite sure how to change that. Any suggestions?

Just adding some debug print statements sheds a little light:

checking if FunctionDecl test::TestClass<float>::multiply is bindable
checking if QualType test::TestClass<float>::vector3_type is bindable
checking if CXXRecordDecl Eigen::Matrix is bindable
checking Eigen::Matrix
Eigen::Matrix point of instantiation is invalid
test::TestClass<float>::multiply return type is not bindable

@mattangus invalid point-of-instantiation usually mean that template type is not fully instantiated. Easiest way to "fix" this would be to add inline function that take this type by-value ie like void foo(MyTemplateType<A, B, C>) {}.
Could you please try this and let me know how it goes? Thanks,

I'm not sure I understood correctly, but that didn't seem to work. Adding this to TestClass.hpp doesn't cause multiply to be generated as a member function.

  void foo(TestClass<float>) {}

generates

void bind_TestClass(std::function< pybind11::module &(std::string const &namespace_) > &M)
{
	{ // test::TestClass file:TestClass.hpp line:10
		pybind11::class_<test::TestClass<float>, std::shared_ptr<test::TestClass<float>>> cl(M("test"), "TestClass_float_t", "");
		cl.def( pybind11::init( [](){ return new test::TestClass<float>(); } ) );
		cl.def( pybind11::init( [](test::TestClass<float> const &o){ return new test::TestClass<float>(o); } ) );
		cl.def_readwrite("amat", &test::TestClass<float>::amat);
		cl.def("scale", (void (test::TestClass<float>::*)(float)) &test::TestClass<float>::scale, "C++: test::TestClass<float>::scale(float) --> void", pybind11::arg("factor"));
	}
	{ // test::TestClassf file:TestClass.hpp line:35
		pybind11::class_<test::TestClassf, std::shared_ptr<test::TestClassf>, test::TestClass<float>> cl(M("test"), "TestClassf", "");
		cl.def( pybind11::init( [](){ return new test::TestClassf(); } ) );
	}
	// test::foo(struct test::TestClass<float>) file:TestClass.hpp line:37
	M("test").def("foo", (void (*)(struct test::TestClass<float>)) &test::foo, "C++: test::foo(struct test::TestClass<float>) --> void", pybind11::arg(""));

}

It seems to be a problem with the template parameter for my class that is used to define the Eigen::Matrix type. I changed this:

    using vector2_type = Eigen::Matrix<T, 2, 1>;
    using vector3_type = Eigen::Matrix<T, 3, 1>;
    using matrix3_type = Eigen::Matrix<T, 3, 3>;

to this:

    using vector2_type = Eigen::Matrix<float, 2, 1>;
    using vector3_type = Eigen::Matrix<float, 3, 1>;
    using matrix3_type = Eigen::Matrix<float, 3, 3>;

and the multiply member function is now generated. It doesn't seem to be a limitation of binder because the binding for foo is generated in this small example:

#pragma once

namespace test {

    template <typename T>
    class Inner {
    private:
    public:
        Inner(T data) : data(data) {}
        T data;
    };

    template <typename T>
    class Outer {
    private:
    public:
        Outer(T data) : inner(data) {}
        Inner<T> inner;
        void foo(Inner<T> d) {
            d.data += 1;
        }
    };

    class Innerf : public Inner<float> {};
    class Outerf : public Outer<float> {};

} // namespace test

which generates:

void bind_NestedTemplate(std::function< pybind11::module &(std::string const &namespace_) > &M)
{
	{ // test::Inner file:NestedTemplate.hpp line:7
		pybind11::class_<test::Inner<float>, std::shared_ptr<test::Inner<float>>> cl(M("test"), "Inner_float_t", "");
		cl.def( pybind11::init<float>(), pybind11::arg("data") );

		cl.def_readwrite("data", &test::Inner<float>::data);
	}
	{ // test::Outer file:NestedTemplate.hpp line:16
		pybind11::class_<test::Outer<float>, std::shared_ptr<test::Outer<float>>> cl(M("test"), "Outer_float_t", "");
		cl.def( pybind11::init<float>(), pybind11::arg("data") );

		cl.def_readwrite("inner", &test::Outer<float>::inner);
		cl.def("foo", (void (test::Outer<float>::*)(class test::Inner<float>)) &test::Outer<float>::foo, "C++: test::Outer<float>::foo(class test::Inner<float>) --> void", pybind11::arg("d"));
	}
	{ // test::Innerf file:NestedTemplate.hpp line:27
		pybind11::class_<test::Innerf, std::shared_ptr<test::Innerf>, test::Inner<float>> cl(M("test"), "Innerf", "");
	}
	{ // test::Outerf file:NestedTemplate.hpp line:30
		pybind11::class_<test::Outerf, std::shared_ptr<test::Outerf>, test::Outer<float>> cl(M("test"), "Outerf", "");
	}
}

So maybe there is something with Eigen that's making things more complex? In theory ther shouldn't be any difference between these two examples because Outer ~= TestClass and Inner ~= Eigen::Matrix.

Sorry for spamming you! I misenterpreted what you were saying. Adding this function:

void foo(TestClass<float>::vector2_type, TestClass<float>::matrix3_type, TestClass<float>::vector3_type) {}

made the multiply function have bindings generated. My question is now: is there any other way to make a valid point of instantiation? This will be quite tedious especially for a matrix class. I will have to define throw away functions for each type and every combination of integers for the size of the matrix. Which will increase the binary size etc.

My question is now: is there any other way to make a valid point of instantiation? This will be quite tedious especially for a matrix class. I will have to define throw away functions for each type and every combination of integers for the size of the matrix. Which will increase the binary size etc.

-- i do not think there is a better way to handle this at least not at the moment.

Re binary size: - well, if we want bindings for particular template (with particular parameters) then we have to instantiate such classes and compile them in advance, - there is simply no other way. This is trade-off between the speed and binary sizes. If you really care about binary sizes and want smallest binary (and smallest memory footprint) then it would be best to use non-template matrix library in such case.

I'm not super worried about the binary size, it was just an example of a limitation of requiring a valid point of instantiation for a template that can have integral values.

Thinking about it a bit more wouldn't the generated code force the valid point of instantiation? Requiring it before the binding code has been generated seems like it's in the wrong order.

Thinking about it a bit more wouldn't the generated code force the valid point of instantiation? Requiring it before the binding code has been generated seems like it's in the wrong order.

-- problem is that in order to generate binding code (which will indeed trigger instantiation) we need to have valid point-of-instantiation so LLVM could provide Binder with information about our template class (it can not do this unless class is fully instantiated). Also please note that we need our class to be instantiated when we parse our all-includes.hpp (not when we already compiling our generated bindings).

Hope this helps,

That makes sense! Thanks!

In case anyone else has the same problem the best work around I found was to create a single function with variabels that require a point of instantiation. It can also be moved outside the namespace that is being bound to, or in an ignored namespace. Like so:

#pragma once

#include <iostream>
#include <Eigen/Dense>

namespace test {

  template <typename T>
  struct TestClass
  {
    //! @brief Types.
    using scalar_type = T;
    using vector2_type = Eigen::Matrix<T, 2, 1>;
    using vector3_type = Eigen::Matrix<T, 3, 1>;
    using matrix3_type = Eigen::Matrix<T, 3, 3>;

    matrix3_type amat;
    inline auto multiply(const vector2_type& x) const -> vector3_type
    {
      return amat * x.homogeneous();
    }

    inline auto mul2(const vector2_type x) const -> void {
      std::cout << x.transpose() << std::endl;
    }

    inline auto scale(T factor) -> void
    {
      amat.block(0, 0, 2, 3) /= factor;
    }
  };

  // is there a better way to do this?
  class TestClassf : public TestClass<float> {};


}  // namespace test

void foo() {
  Eigen::Matrix<T, 2, 1> v1;
  Eigen::Matrix<T, 3, 1> v2;
  Eigen::Matrix<T, 3, 3> v3;
}

Macros could also be used to make for less repetition. This also doesn't seem to increase the binary size at all.