有没有一种方法可以从传递给C API的函数指针中修改类状态

Is there a way to modify class state from a function pointer passed to a C API?

本文关键字:API 函数 指针 状态 修改 一种 方法 有没有      更新时间:2023-10-16

我有一个类,它有一些内部状态。该类使用一个名为GLFW的C库来处理窗口管理和键盘输入。

这个类看起来像这样:

class Object {
public:
    Object(...);
    ...
private:
    int _member_variable; // Internal state
    GLFWwindow *_window;
    ...
}

GLFW背景

GLFW是一个处理窗口处理和键盘输入的C库。它使用回调函数来报告用户在窗口聚焦时按键的时间。

void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods)
{
    printf("User pressed key #%dn", key);
}

要将回调函数绑定到窗口,可以使用名为glfwSetKeyCallback的函数。

// This will bind the `key_callback` function to `window`.
glfwSetKeyCallback(window, key_callback);

我遇到的问题

我想通过键回调函数修改类的内部状态,而不依赖于static存储的状态。我不能具有全局状态,因为我希望能够创建Object的多个实例,并使它们相互独立地工作。

假设在这种情况下,每当用户按下某个键时,我都希望增加_member_variable

我想做这样的事情:

// Private method for dealing with key presses.
void Object::key_callback(GLFWwindow *window, int key, int scancode, int action, int mods)
{
    if (action == GLFW_PRESS)
        ++_member_variable; // Increment the internal state
}
Object::Object(GLFWwindow *window, ...)
: _window(window), ...
{
    glfwSetKeyCallback(window, key_callback);
}

但这不起作用,因为Object::key_callbackObject的成员函数,并且需要不可见的this参数。(Object::*)(GLFWwindow *, int, int, int, int)无法转换为(*)(GLFWwindow *, int, int, int, int)

我尝试过的

  • 无法将私有成员函数传递给GLFW API。考虑到成员函数需要不可见的this参数,这是可以理解的
  • 我还尝试过使用lambda来捕获我想要的状态,但也没有成功

有没有一种方法可以在不依赖全局状态的情况下修改传递给C API的函数的类状态?就像在这种情况下从传递给glfwSetKeyCallback的函数中递增_member_variable一样?

是的,这是可能的。该解决方案是GLFW的API支持的解决方案。

典型的C库允许您在注册回调时向用户数据传递不透明的指针。glfwSetKeyCallback没有。

相反,在GLFW中,您可以调用glfwSetWindowUserPointer来在GLFWWindow中设置一个用户指针字段。在Object构造函数中或在实际创建窗口的任何位置将其设置为this。然后,在Object类中添加一个静态成员函数。此功能可以是私有的。将其作为回调传递给glfwSetKeyCallback。调用此回调时,使用glfwGetWindowUserPointerGLFWWindow检索用户指针,将其强制转换为Object*,并使用它将调用分派给该对象的非静态成员函数或直接修改数据成员。

http://www.glfw.org/docs/latest/window.html#window_userptr

编辑

为了使这个答案更加完整,让我评论一下你尝试的解决方法。

  • 无法将私有成员函数传递给GLFW API。考虑到成员函数需要不可见的this参数,这是可以理解的
  • 我还尝试过使用lambda来捕获我想要的状态,但也没有成功

确实可以像我和@derhass所描述的那样(几乎同时(传递一个私有的静态成员函数。不过,我确信你指的是非静态成员函数。正如您所观察到的,您将需要this指针来调度到正确的对象。看起来可以使用glfwSetWindowUserPointer设置this指针,然后将指针传递给私有的非静态成员函数。然而,这是行不通的。指向成员函数的指针通常被实现为"胖"指针,除了地址之外还有额外的数据。例如,请参阅本讨论。如果您设法将一个强制转换为普通指针并将其传递给C库,您将以某种方式得到截断。即使可以避免强制转换截断(我认为这是不可能的(,C库也没有足够的存储空间来存储胖指针,也无法将其原封不动地返回给您。

同样,lambdas也是如此。

这个答案是特定于GLFW库的,并不是试图回答更通用的编程语言问题。然而,你可能会发现这很有用,因为它解决了你真正想要解决的问题。

GLFW确实支持一种方式,允许通过每个窗口指定的用户指针将用户特定的数据传递给回调。

某种使用方法可能是这个

class Object {
public:
    Object(...);
    ...
private:
    static void keyboard_callback_glfw(GLFWwindow *win_glfw, int key, int scancode, int action, int mods); // raw GLFW callback
    int _member_variable; // Internal state
    GLFWwindow *_window;
    ...
protected:
    void onKeyPress(int key, int scancode, int action, int mode); // object-oriented callback that does the work
}
void Object::keyboard_callback_glfw(GLFWwindow *win, int key, int scancode, int action, int mode)
{
     Object *obj=static_cast<Object*>(glfwGetWindowUserPointer(win));
     // call the Objects onKey() handler
     obj->onKeyPress(key, scancode, action, mode);
}

这个方案在继承方面也很好地工作,所以你基本上可以将GLFW及其所有回调包装在某个"Window"类中并从中派生,覆盖各种onEvent处理程序,而不必再关心GLFW的C-API。在GLFW回调注册之前,您只需要在对象中创建(或分配给(窗口后glfwSetWindowUserPointer(_window, this);即可。

回调的第一个参数是指向GLFWwindow的指针。您可以使用它来标识您的C++窗口对象,例如通过std::unordered_map(然后需要具有静态存储或从静态存储中引用(。然后将调用传递给一个合适的方法。

注意线程问题。

此外,提前考虑创建窗户和破坏窗户的责任也是个好主意。GUI窗口通常是自测试的,而且很可能你的C窗口API也是这样。我或多或少地已经掌握了启动API级自我构建的技术,并让它导致C++对象的破坏,而不是相反,因为它产生了最不复杂的代码,但无论你选择什么,在做出设计选择之前,最好先考虑一下这一点。

我可能错了,但我不知道如果没有一些全局状态,你如何才能完成你想要做的事情。可以创建一个全局变量,该变量存储指向Object实例的指针,然后在key_callback运行时调用其成员函数。这样,您至少可以控制损坏,因为您只有一个全局变量,然后不必使Object的每个实例都是全局变量。

这里有一些代码演示了我要说的内容:

// the only global variable in the code:
std::list<Object *> registeredObjects;
// register your callback to a free function instead of a member function
void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods)
{
    // however, make this function call the member function of your Objects
    for (Object *registeredObject : registeredObjects)
        registeredObject->key_callback(window, key, scancode, action, mods);
}
// call this function to make your Objects able to react to key presses
void registerKeyCallback(Object *objectToRegister)
{
    for (Object *obj : registeredObjects)
        if (obj == objectToRegister) // is the given object already registered?
            return; // yes, so don't register again
    registeredObjects.push_back(objectToRegister);
}

有了这段代码,您可以调用registerKeyCallback函数,传递您希望能够对按键做出反应的Object的地址,然后Object将能够在对按键做出响应时更改其自身的内部状态。如果Object::key_callback函数是私有的,那么您可以使key_callback函数成为您类的朋友,如下所示:

class Object
{
    ...
    friend void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods);
};

然后,key_callback函数将能够调用专用Object::key_callback函数。