【问题标题】:How can I make a function that returns a function?如何创建一个返回函数的函数?
【发布时间】:2016-10-12 13:35:41
【问题描述】:

大图:我有一个带有函数的模块和一个带有这些函数的过程和函数的模块。

当我结合两个函数时(来自函数的模块接口):

double f1(double alpha, double x);
double f2(double beta, double x);

在几个方面,(其中之一是添加):

double OP_Addition(double (*f)(double,double) , double (*g)(double,double), double param1, double param2, double x);

对以下(部分)实现没有任何问题:

z1 = (*f)(param1, x);
z2 = (*g)(param2, x);
y = z1 + z2;
return y;

但是当我想返回一个指向“新”函数的指针时,类似于:

void *OP_PAdd( double (*f)(double,double), double param3 );

我不能让它正常工作,也不能做出正确的“调用”。我想在其他函数中使用输出“函数”作为输入。

【问题讨论】:

  • 我不明白您要做什么以及“无法使其正常工作”的意思。
  • 请注意,C 没有 lambdas(AKA 匿名函数)。您可以在 C 中返回指向函数的指针,但该函数必须在编译时声明。例如。您不能接受一个整数并返回一个将此类整数添加到其参数的函数。或者取两个函数指针并返回一个指向这两个函数的逐点和的指针。可能有一些用于 lambdas(或部分应用程序)的不可移植库,并且 lambdas 现在在 C++ 中内置,但在 C 中没有内置支持。
  • @chi 。 WG14 似乎喜欢 this proposal 将 lambdas 添加到 C2X 语言中。
  • 你为什么要问,你有什么实际应用?
  • 函数不是 C 中的一阶对象。您可以传递指向现有函数的指针,但函数本身不是可以通过值创建或传递的对象。我怀疑您可能来自函数式或动态类型语言背景(OCaml、Haskell、Python、Ruby、Lisp),其中函数一阶对象。请注意,这不是命令式低级编程的固有限制。还有其他编译的系统编程语言,其中函数是一阶对象。同样正如@chi 指出的那样,有人提议将其添加到 C2x。

标签: c function pointers modularity


【解决方案1】:

当从另一个函数返回一个函数时,最简洁的方法是使用typedef

typedef double (*ftype)(double, double);

然后你可以像这样声明你的函数:

ftype OP_PAdd( ftype f, double param3 )
{
    ....
    return f1;
}

你可以在没有typedef 的情况下做到这一点,但它很乱:

double (*OP_PAdd( double (*f)(double,double), double param3 ))(double,double)
{
    return f1;
}

因此,当您将函数指针作为参数或其他函数的返回值时,请使用typedef

编辑:

虽然你可以像这样声明类型:

typedef double ftype(double, double);

在实践中你永远不能直接使用这样的类型。一个函数不能返回一个函数(只有一个指向函数的指针),并且这个类型的变量不能被赋值。

此外,您不需要显式取消引用函数指针来调用函数,因此隐藏指针本身并不是一个大问题。将函数指针定义为typedef 也是惯例。来自man page for signal

   #include <signal.h>

   typedef void (*sighandler_t)(int);

   sighandler_t signal(int signum, sighandler_t handler);

【讨论】:

  • 我不喜欢将指针隐藏在 typedef 后面。我认为最好将ftype 设为实际的函数类型,然后返回ftype *
  • 一般来说,是的,在 typedef 中隐藏指针并不是一个好主意。但是函数的情况,这是不可避免的。没有函数类型之类的东西,只有指向函数的指针。
  • 废话:typedef double ftype(double, double); ftype *OP_PAdd(ftype *f, double param3);
  • @melpomene;此外,在这种情况下,typedefing 函数使代码更干净,更具可读性。
  • @melpomene 参数中的函数会自动调整为函数指针。
【解决方案2】:

其他答案是正确且有趣的,但您应该知道,在便携式 C99 中,没有办法将真正的 closures 作为 C 函数(这是一个基础 C) 的限制。如果你不知道闭包是什么,请仔细阅读 the wiki 页面(同时阅读 SICP,尤其是 §1.3)。但是请注意,在C++11 中确实有闭包,使用std::functionlambda-expressions。大多数其他编程语言(Ocaml、Haskell、Javascript、Lisp、Clojure、Python 等)都有闭包。

由于 C 语言中缺乏真正的闭包(“数学上”,C 函数中唯一的闭包值是全局变量或静态变量或文字),大多数接受 C 函数指针的库都提供了一个 API 处理 callbacks 和一些客户端数据(一个简单的例子可能是qsort_r,但更认真地看看内部GTK)。该客户端数据(通常是不透明的指针)可用于保持封闭值。您可能希望遵循类似的约定(因此系统地将函数指针作为带有一些附加客户端数据的回调传递),因此您需要更改 C 函数的签名(而不是仅传递原始函数指针,您将传递函数指针和一些客户端数据作为回调,以“模拟”闭包)。

您有时可以在运行时生成 C 函数(使用非标准功能,可能在操作系统或某些外部库的帮助下)。您可能会使用一些 JIT compiling 库,例如 GNU lightninglibjit(两者都会快速生成一些运行缓慢的代码)、asmjit(您将显式生成每条机器指令,快速发出是您的责任x86-64 代码)、GCCJITLLVM(两者都在现有编译器之上,因此可用于发出 - 有点慢 - 一些优化的代码)。在 POSIX 和 Linux 系统上,您还可以在某个临时文件 /tmp/tempcode.c 中发出一些 C 代码,将该代码的编译(例如 gcc -fPIC -Wall -O2 -shared /tmp/tempcode.c -o /tmp/tempcode.so)分叉到插件中,并使用 dlopen(3) 和 @987654338 动态加载生成的插件@..

顺便说一句,我们不知道您正在编写的实际应用程序是什么,但您可以考虑在其中嵌入一些解​​释器,例如LuaGuile。然后,您将使用并向嵌入式评估器/解释器提供回调。

【讨论】:

  • DV 因为“...没有办法将真正的闭包作为 C 函数” - 我只是解释了如何做到这一点,而不依赖于机器相关的内联汇编,并且只使用标准函数。
  • 我投了反对票,因为我觉得当他们说“新”(他们的引号,不是我的)时,你认为他们的意思是新生成的,以及对闭包和其他语言的一般讨论 - 因为他们将 C 指定为一个标签,这在很大程度上是无关紧要的,尽管它很有趣:)。我确实很欣赏加载新编译的模块,就像我过去所做的那样。如果您添加了与@dbush 的答案类似的内容,我会删除反对票。
【解决方案3】:

你的意思是这样的吗? decider() 函数返回一个指向另一个函数的指针,然后调用该函数。

#include <stdio.h>
#include <stdlib.h>

typedef double(*fun)(double, double);

double add(double a, double b) {
    return a + b;
}

double sub(double a, double b) {
    return a - b;
}

double mul(double a, double b) {
    return a * b;
}

fun decider(char op) {
    switch(op) {
        case '+': return add;
        case '-': return sub;
        case '*': return mul;
    }
    exit(1);
}

int main(void)
{
    fun foo;

    foo = decider('+');
    printf("%f\n", foo(42.0, 24.0));

    foo = decider('-');
    printf("%f\n", foo(42.0, 24.0));

    foo = decider('*');
    printf("%f\n", foo(42.0, 24.0));

    return 0;
}

程序输出:

66.000000
18.000000
1008.000000

编辑:@dbush 答案 下的 cmets 之后,此版本从 typedef 作为指针退回到只是一个函数。它给出了相同的输出,但在decider() 中它编译干净并给出正确的输出,无论我写的是return add; 还是return &amp;add;

#include <stdio.h>
#include <stdlib.h>

typedef double(fun)(double, double);

double add(double a, double b) {
    return a + b;
}

double sub(double a, double b) {
    return a - b;
}

double mul(double a, double b) {
    return a * b;
}

fun *decider(char op) {
    switch(op) {
        case '+': return add;     // return &add;
        case '-': return sub;
        case '*': return mul;
    }
    exit(1);
}

int main(void)
{
    fun *foo;

    foo = decider('+');
    printf("%f\n", foo(42.0, 24.0));

    foo = decider('-');
    printf("%f\n", foo(42.0, 24.0));

    foo = decider('*');
    printf("%f\n", foo(42.0, 24.0));

    return 0;
}

【讨论】:

  • C 在将函数类型转换为函数指针类型方面非常松散。例如,要获取指向函数的指针,可以单独使用函数名,也可以应用地址运算符&amp;,这两种方式都可以。类似地,当通过函数指针调用函数时,您可以在调用函数时显式取消引用该指针,或者您可以省略*,它同样可以使用任何一种方式。如果 C 更严格地要求地址和取消引用运算符,我个人更喜欢它,但由于历史原因,它不是。
  • @WeatherVane:不确定我明白你的意思。 str&amp;str do 产生不同的结果!很可能不是值(但即使不能保证),而只是类型,但它们差别很大。
  • @WeatherVane:再说一遍:它可能给出相同的汇编代码,但结果不同。函数名将被转换为指向该函数的指针,&amp;func 也是如此。对于数组,类型将不相同。所以不同的语义和任何更好的编译器都会警告数组案例的类型冲突。
  • @WeatherVane 编译器知道区别。后者会产生警告:format ‘%s’ expects type ‘char *’, but argument 2 has type ‘char (*)[6]’
  • @WeatherVane:从语言方面来说是错误的。并且实现可能会生成不同的值(例如,如果类型在指针中编码 - 对于当前系统来说不太可能,但允许)。此外,编译器可能会生成任何类型的代码,因为这会调用未定义的行为。对于一个函数,func&amp;func here 都定义了完全相同的结果(即包括相同的类型)。事情有所不同,例如sizeof 其中sizeof(func) 是每个定义的UB。
【解决方案4】:

在C中,你可以返回指向函数的指针,但要做到这一点,函数需要首先存在,动态创建函数并不是C说的可以的,别管怎么做

如果您的代码只能在一个操作系统和一个处理器上运行(可能还有其他一些限制),您可以:

  1. 分配内存页
  2. 写数据和机器代码做你想做的事,调用指针传递的函数等等。
  3. 将内存保护从读/写更改为读/执行
  4. 返回指向创建函数的指针
  5. 不用担心每个函数需要 4kB

可能有一些库可以做到这一点,但不一定是可移植的

【讨论】:

  • 一些库能够生成新的 C 函数(使用不可移植的技巧或操作系统帮助)。见my answer
【解决方案5】:

所以你想要一个函数返回一个指向函数的指针。

double retfunc()
{
   return 0.5;
}

double (*fucnt)()
{
  return retfunc;
}

main()
{
   printf("%f\n", (*funct())());
}

【讨论】:

    【解决方案6】:

    由于有些人显然对编写 hack 来解决这个问题感到偏执,这里有一个不那么 hack 的方法:使用带有 setjmp 和 longjmp 的静态结构。

    jmp_buf jb;
    
    void *myfunc(void) {
        static struct {
            // put all of your local variables here.
            void *new_data, *data;
            int i;
        } *_;
        _ = malloc(sizeof(*_));
        _.data = _;
        if (!(_.new_data = (void *)(intptr_t)setjmp(jb)))
            return _.data;
        _.data = _.new_data;
        /* put your code here */
        free(_);
        return NULL;
    }
    

    为了解释这里发生了什么,setjmp 将在创建跳转缓冲区时返回值 0,否则它将返回 longjmp 传递的值(例如 longjmp(jb, 5) 将导致 setjmp 返回 5) .

    所以,我们正在做的是让我们的函数返回一个指向它分配的数据结构的指针;然后像这样调用我们的闭包:

    void *data = myfunc();
    longjmp(jb, (int)(intptr_t)data);
    

    请注意,不能保证 int 大到足以在所有平台上存储指针;因此您可能需要创建一个数据池并通过句柄(池中的索引)返回/传递数据。

    正如我之前所说,闭包只是一个函数,它的所有数据都分配在堆上。

    多年来,我一直在为 N64 和 PSP 游戏编写 hack。声称这是不可能的人可能从未修补过这种东西。主要归结为缺乏经验。

    【讨论】:

    • 有趣的破解。不幸的是,它不符合标准。按照标准,setjmp 不能出现在赋值表达式中,longjmp 可能不会在现在已死的函数范围内引用 jmp_buf 初始化。此外,您至少应该确保 __Thread_localvolatile
    • 好点。无论如何,还有其他方法可以在 C 中实现闭包。我不会说它们很漂亮,但这个概念很简单。只需为堆上的关闭分配状态。实际上,我很久以前就使用过同样的技巧,将“this”参数隐式传递给函数以使用 C 中的假类。当时没有考虑过,但从技术上讲它是闭包。
    • 您的方法不是创建一个函数(指针),而是创建一个需要与普通函数(指针)不同处理的对象。如果可以承认这是一种解决方案,为什么不使用包含函数指针和某些状态的结构并将其称为someStruct-&gt;funPtr(someStruct) 来实现闭包呢?我不明白你为什么需要这些复杂的东西。
    • 由于“你一直在写 PSP hack”和“归结为缺乏经验”,我希望你能接受来自主要 HBL 开发人员之一的一些反馈(回到 MoHH/ Patapon 利用天)。在 C 中没有可移植的方式。其他评论者已经强调了您方法中的缺陷。唯一的方法是 1) 使用全局变量来保持分配状态(可移植,但再见多线程)或 2) 动态生成程序集蹦床(特定于架构和操作系统,因为您需要 executable 内存) .当然,您会将所有内容都包含在#defines 中以保持理智。
    • 顺便说一句,这仅在您的函数非常简单(没有堆栈状态,所有内容都在寄存器中)或在myfunc()longjmp(...) 之间未触及堆栈时才有效,因此保存的 SP 仍然指向有效的本地状态。在您的示例中,这取决于有多少堆栈 longjmp 垃圾。 (这是因为你跳到了一个死范围)
    【解决方案7】:

    我会在这里变得很老套,所以抓住你的马裤。

    标准 C api 带有两个函数,称为 setjmplongjmp。除了糟糕的命名之外,它们本质上所做的是将当前状态(包括堆栈位置和寄存器值)的副本存储到 jmp_buf(或者,技术名称,continuation)中。

    现在,假设您创建了一个函数:

    jmp_buf jb;
    
    void sfunc(void) {
        void *sp_minus1 = 0xBEEFBABE;
        setjmp(jb);
    }
    

    当你调用 sfunc 时,会创建一个栈帧。因为这个函数没有参数,堆栈中的第一个条目将是返回地址,紧随其后的是 sp_minus1 对象。

    为什么这是相关的?嗯,sp_minus1 的地址是相对于堆栈帧的开始的。如果你能在jb找到栈帧的地址,你可以把它改成……比如说,堆中的某个位置?

    我们现在得到的是一种为堆上的 longjmp 函数调用创建堆栈帧的方法,该堆栈帧可以包含有关调用它们的上下文的附加状态;或者换句话说,闭包。

    我认为我从未见过有人以这种方式使用 longjmp/setjmp,但如果您正在寻找一种在 C 中动态生成和返回函数的方法,我认为这将是您的最佳途径。

    编辑:

    这是我所描述的 hack 的示例实现:

    #include <inttypes.h>  // intptr_t
    #include <setjmp.h>    // longjmp, setjmp
    #include <stdio.h>     // printf
    #include <stdlib.h>    // malloc, free
    #include <string.h>    // memcpy
    
    
    typedef struct {
        jmp_buf jb;
        int fixupc;
        int fixupv[10];
        size_t stack_size;  // this is only an approximation
        void *stack_ptr;
    } CLOSURE;
    
    
    int getclosure(CLOSURE *closure) {
        unsigned int i, size;
        void *i_ptr = &i, *sp;
        unsigned char *data = (unsigned char *)(void *)closure->jb;
        memset(closure, 0, sizeof(CLOSURE));
        if (!setjmp(closure->jb)) {
            printf("looking for 0x%08X...\n\n", (unsigned int)(intptr_t)i_ptr);
            for (i = 0; i < sizeof(closure->jb); i++) {
                memcpy(&sp, &data[i], sizeof(void *));
                size = (unsigned int)(intptr_t)(sp - i_ptr);
                if (size < 0x300) {
                    closure->fixupv[closure->fixupc++] = i;
                    printf("  fixup @ 0x%08X\n", (unsigned int)(intptr_t)sp);
                    if (sp > closure->stack_ptr) {
                        closure->stack_size = size;
                        closure->stack_ptr = sp;
                    }
                }
            }
            if (!closure->stack_ptr)
                return 0;
            printf("\nsp @ 0x%08X\n", (unsigned int)(intptr_t)closure->stack_ptr);
            printf("# of fixups = %i\n", closure->fixupc);
            /*
             * once we allocate the new stack on the heap, we'll need to fixup
             * any additional stack references and memcpy the current stack.
             *
             * for the sake of this example, I'm only fixing up the pointer
             * to the stack itself.
             *
             * after that, we would have successfully created a closure...
             */
             closure->stack_size = 1024;
             sp = malloc(closure->stack_size);
             memcpy(sp, closure->stack_ptr, closure->stack_size);
             memcpy(&data[closure->fixupv[0]], &sp, sizeof(void *));
             closure->stack_ptr = sp;
             return 1;
        } else {
            /*
             * to this bit of code right here
             */
            printf("holy shit!\n");
            return 0;
        };
    }
    
    void newfunc(CLOSURE *closure) {
        longjmp(closure->jb, 1);
    }
    
    void superfunc(CLOSURE *closure) {
        newfunc(closure);
    }
    
    int main(int argc, char *argv[]) {
        CLOSURE c;
        if (getclosure(&c)) {
            printf("\nsuccess!\n");
            superfunc(&c);
            free(c.stack_ptr);
            return 0;
        }
        return 0;
    }
    

    这在技术上是一种堆栈粉碎形式,因此默认情况下,GCC 会生成堆栈金丝雀,从而导致程序中止。如果你用'-fno-stack-protection'编译,它会起作用。

    【讨论】:

    • 我不想想这会打开什么样的蠕虫罐头。但话又说回来,也许我害怕它,因为我不知道。
    • @DeftlyHacked 有趣!我将不得不玩弄这个。顺便说一句,我虽然你对你评论的帖子投了反对票。
    • 嗯,我是想这么做的,但当时我实际上并没有这个特权。如果您觉得这很有趣,您可能还会发现可以使用相同的技术来创建漏洞利用也很酷。 Setjmp/Longjmp 几乎间接地使您可以控制堆栈指针,一旦您拥有它,就可以执行任意代码。我相信 libpng 曾经被用来利用这样的游戏机。
    • 这是一个特定于实现的技巧。正如您所说,一些有效的编译器或选项 (-fstack-protection) 不允许这种技巧。
    • @BasileStarynkevitch,如果你懒得看帖子的最后一行,我特别指出,“这在技术上是一种堆栈粉碎,所以默认情况下,GCC 会生成堆栈金丝雀,导致程序中止。如果你用'-fno-stack-protection'编译,它会工作。“。 OP 只是询问如何返回指向新函数的指针;他并没有说它不能涉及禁用安全预防措施和破解堆栈指针。几乎所有的 C 线程库都使用相同的技巧(例如 libmill);他们只是在汇编而不是 C 中完成。
    猜你喜欢
    • 2012-09-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-02
    • 2011-02-19
    • 1970-01-01
    相关资源
    最近更新 更多