pybind11 icon indicating copy to clipboard operation
pybind11 copied to clipboard

[BUG]: can not access the target (std::function) of python functions

Open jagiella opened this issue 3 years ago • 2 comments

Required prerequisites

  • [X] Make sure you've read the documentation. Your issue may be addressed there.
  • [X] Search the issue tracker and Discussions to verify that this hasn't already been reported. +1 or comment there if it has.
  • [X] Consider asking first in the Gitter chat room or in a Discussion.

Problem description

Motivation:

For callback handling I would like to be able to compare callbacks / functions passed from python via pybind to a c++ implemented comparison function.

  • callbacks could be either "pure" python functions or pybind-wrapped c++ functions
  • comparisons should be possible between all combinations of function types (python vs python, python vs c++, c++ vs c++)

Solution:

To compare 2 std::function instance one needs to actually compare their targets (=function pointer):

bool equal(std::function<void()> f1, std::function<void()> f2) {
	if (f1.target_type() != f2.target_type()){
		std::cout << "f1 and f2 of different types: " << f1.target_type().name() << " vs " << f2.target_type().name() << std::endl;
		return false;
	}else{
		std::cout << "f1 and f2 of same type: " << f1.target_type().name() << std::endl;
	}

	auto **p_f1 = f1.target<void(*)()>();
	auto **p_f2 = f2.target<void(*)()>();
	if(p_f1==0){
		std::cout << "f1 can not be cast to : void(*)()" << std::endl;
		return false;
	}
	if(p_f2==0){
		std::cout << "f2 can not be cast to : void(*)()" << std::endl;
		return false;
	}

	if (*p_f1 != *p_f2)
		return false;

	return true;
}

PYBIND11_MODULE(example, m) {
	m.def("equal", &equal, "A function that compares two functions");
}

Problem:

When passing two different or twice the same std::function instance to the function above, the comparison works correctly as expected. But when passing a "pure" python function, it's target can not be cast to the correct function type/pointer (= void(*)()). The target will always point to address 0.

Reproducible example code

# test.py (test script)

from example import equal, cpp_callback1, cpp_callback2

def py_callback1():
    print('py_callback1 called')

def py_callback2():
    print('py_callback2 called')

print(equal(cpp_callback1, cpp_callback1))
print(equal(cpp_callback1, cpp_callback2))
print(equal(py_callback1, py_callback1))
print(equal(py_callback1, py_callback2))
// example.cpp (pybind code)
#include <functional>
#include <iostream>
#include <pybind11/pybind11.h>
#include <pybind11/functional.h>

namespace py = pybind11;

bool equal(std::function<void()> f1, std::function<void()> f2) {
	if (f1.target_type() != f2.target_type()){
		std::cout << "f1 and f2 of different types: " << f1.target_type().name() << " vs " << f2.target_type().name() << std::endl;
		return false;
	}else{
		std::cout << "f1 and f2 of same type: " << f1.target_type().name() << std::endl;
	}

	auto **p_f1 = f1.target<void(*)()>();
	auto **p_f2 = f2.target<void(*)()>();
	if(p_f1==0){
		std::cout << "f1 can not be cast to : void(*)()" << std::endl;
		return false;
	}
	if(p_f2==0){
		std::cout << "f2 can not be cast to : void(*)()" << std::endl;
		return false;
	}

	if (*p_f1 != *p_f2)
		return false;

	return true;
}

void cpp_callback1() {
	std::cout << "cpp_callback1 called" << std::endl;
}
void cpp_callback2() {
	std::cout << "cpp_callback2 called" << std::endl;
}

PYBIND11_MODULE(example, m) {
	m.def("equal", &equal, "A function that compares two functions");

	m.def("cpp_callback1", &cpp_callback1, "A test callback function");
	m.def("cpp_callback2", &cpp_callback2, "An other callback function");
}

jagiella avatar Apr 26 '22 22:04 jagiella

This is not a bug in pybind11, it's just how std::function works. .target<void(*)()> will only work if the std::function was created from a pointer to a free-function whose signature was void(). It does not work if the std::function<> was created from a lambda, for a functional structure, etc.

There is no way to compare any two std::function because you have to know the underlying type from which the function was created.

If your use case is limited to free-function with void() signature and pure python function, then you can probably simply takes py::objetct as parameters and compare them.

Holt59 avatar Apr 29 '22 09:04 Holt59

The use case is to (un)register and trigger std::functions as callbacks in c++ implementation of an Event/Signal class. The pybinding is just an extension to allow 1. usage (e.g. triggering) on the python side, but 2. also to allow registering python function(al)s to the (c++) Event class.

jagiella avatar May 11 '22 10:05 jagiella