在运行时创建一个具有唯一函数指针的函数

3
当调用需要回调函数作为参数的WinAPI函数时,通常会有一个特殊参数来传递一些任意数据给回调函数。如果没有这样的参数(例如SetWinEventHook),我们唯一能够理解哪个API调用导致了给定回调的调用的方法是具有不同回调函数。当我们在编译时知道所有调用给定API的情况时,我们可以始终创建一个带有静态方法的类模板,并在不同的调用方实例化它以使用不同的模板参数。那是一项可怕的工作,我不喜欢这样做。
如何在运行时创建回调函数,使它们具有不同的函数指针?
我看到了一个解决方案(抱歉,是俄语),它使用运行时汇编生成,但它不可移植到x86 / x64架构。

1
在运行时完成这个操作几乎需要进行运行时代码生成。 - cdhowie
@cdhowie 我也这么认为。我希望有人能猜出另一种选择(可能有一个快速且不太规范的WinAPI hack,比如运行时DLL实例化),或者链接到一个生产就绪的运行时代码生成器。 - polkovnikov.ph
这个回答有用吗?https://dev59.com/-GfWa4cB1Zd3GeqPjrjC#12136310 - sehe
后来的评论指出该答案在64位上存在各种问题。 - arx
@arx 实际上,无法使其工作 :) - polkovnikov.ph
2个回答

2
我想到了一个解决方案,应该是可移植的(但我还没有测试过):
#define ID_PATTERN          0x11223344
#define SIZE_OF_BLUEPRINT   128   // needs to be adopted if uniqueCallbackBlueprint is complex...

typedef int (__cdecl * UNIQUE_CALLBACK)(int arg);


/* blueprint for unique callback function */
int uniqueCallbackBlueprint(int arg)
{    
    int id = ID_PATTERN;

    printf("%x: Hello unique callback (arg=%d)...\n", id, arg);

    return (id);
}


/* create a new unique callback */
UNIQUE_CALLBACK createUniqueCallback(int id)
{
    UNIQUE_CALLBACK result = NULL;
    char *pUniqueCallback;
    char *pFunction;
    int pattern = ID_PATTERN;
    char *pPattern;
    char *startOfId;
    int i;
    int patterns = 0;


    pUniqueCallback = malloc(SIZE_OF_BLUEPRINT);

    if (pUniqueCallback != NULL)
    {
        pFunction = (char *)uniqueCallbackBlueprint;
#if defined(_DEBUG)
        pFunction += 0x256;  // variable offset depending on debug information????
#endif /* _DEBUG */
        memcpy(pUniqueCallback, pFunction, SIZE_OF_BLUEPRINT);
        result = (UNIQUE_CALLBACK)pUniqueCallback;


        /* replace ID_PATTERN with requested id */
        pPattern = (char *)&pattern;
        startOfId = NULL;
        for (i = 0; i < SIZE_OF_BLUEPRINT; i++)
        {
            if (pUniqueCallback[i] == *pPattern)
            {
                if (pPattern == (char *)&pattern)
                    startOfId = &(pUniqueCallback[i]);
                if (pPattern == ((char *)&pattern) + sizeof(int) - 1)
                {
                    pPattern = (char *)&id;
                    for (i = 0; i < sizeof(int); i++)
                    {
                        *startOfId++ = *pPattern++;
                    }
                    patterns++;
                    break;
                }

                pPattern++;
            }
            else
            {
                pPattern = (char *)&pattern;
                startOfId = NULL;
            }
        }

        printf("%d pattern(s) replaced\n", patterns);

        if (patterns == 0)
        {
            free(pUniqueCallback);
            result = NULL;
        }
    }

    return (result);
}

使用方法如下:

int main(void)
{
    UNIQUE_CALLBACK callback;
    int id;
    int i;

    id = uniqueCallbackBlueprint(5);
    printf("  -> id = %x\n", id);

    callback = createUniqueCallback(0x4711);
    if (callback != NULL)
    {
        id = callback(25);
        printf("  -> id = %x\n", id);
    }

    id = uniqueCallbackBlueprint(15);
    printf("  -> id = %x\n", id);

    getch();
    return (0);
}

我发现一个有趣的行为,如果使用调试信息编译(Visual Studio),则pFunction = (char *)uniqueCallbackBlueprint;获取的地址会偏移若干字节。可以使用显示正确地址的调试器来获得此差异。这个偏移量从构建到构建不同,我认为这与调试信息有关?对于发布版本没有问题。因此,也许应该将其放入作为“发布”构建的库中。

另一件要考虑的事情是pUniqueCallback的字节对齐可能会成为问题。但是,将函数开头对齐到64位边界并不难添加到此代码中。

pUniqueCallback内,您可以实现任何您想要的内容(注意更新SIZE_OF_BLUEPRINT以便不会错过函数尾部)。函数被编译并且生成的代码在运行时被重复使用。创建唯一函数时,id的初始值被替换,因此原始函数可以处理它。


看起来很有趣。像这样替换唯一的模式出于许多原因非常危险,但这可能有效。让我考虑一下这个问题。 - polkovnikov.ph
这与顶部链接的答案存在相同的问题。64位结构化异常处理是基于表格的。所有代码都需要表项来描述在发生异常时如何展开堆栈。如果您在运行时生成或复制代码而没有更新SEH表,则应用程序可能会以奇怪的方式崩溃。 - arx

2
您可以使用libffi的闭包API。它允许您创建具有不同地址的跳板。我在这里实现了一个包装类here,但那还没有完成(只支持int参数和返回类型,您可以专门化detail::type以支持更多不仅仅是int的类型)。更重量级的替代方案是LLVM,但如果您只处理C类型,libffi就足够了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接