在C语言中,如何使用一个变量参数的列表来构建函数调用?

20

假设有一些参数以某种方式存储,例如在数组中。

给定一个函数指针,我如何调用它并传递存储的参数列表?

我不是试图将数组作为参数传递。你理解了没,好吧?我想逐个将其元素作为参数传递。数组只是为了说明,我可以将参数存储在某些元组结构中。此外,请注意,我手头有一个函数指针,并且可能具有字符串格式的签名。我不只是尝试定义一个能够处理可变参数列表的函数。

我唯一知道的方法是使用汇编(例如通过 __asm push 等)或者这样:

void (*f)(...);

int main()
{
    f = <some function pointer>;
    int args[]; <stored in a array, just to illustrate>
    int num_args = <some value>;

    switch(num_args)
    {
        case 0:
            f();
        break;

        case 1:
            f(args[0]);
        break;

        case 2:
            f(args[0], args[1]);
        break;

        /* etc */
    }

    return 0;
}

我对这种方法不太满意...

有没有另一种更短小、更易携带的方式?

多种脚本语言可以调用C函数。

Python或Ruby等脚本语言是如何做到的?它们如何以可移植的方式实现?它们只是在几个平台上使用汇编,还是以上述方式结束?

请注意,我真的不是在问关于从脚本语言到C的参数整理和其他细节的问题,我只是对内部如何建立脚本语言对C函数的调用感兴趣。

编辑

我将保留问题的标题,但我认为一个更好的提问方式是:

如何仅在运行时使用其指针和签名来调用C函数?

更新

来自PLT Scheme Foreign Interface:

调用是一个普通的函数调用。在动态环境中, 我们创建一个“调用接口”对象,该对象指定(二进制) 输入/输出类型;此对象可与任意函数指针和输入值数组一起使用,以执行对函数的调用并检索其结果。这样做需要操纵堆栈并知道如何调用函数,这些都是 libffi 所处理的细节。

感谢@AnttiHaapala进行搜索、查找和指示libffi。这就是我要寻找的东西,它被一堆脚本语言使用,是一个便携式库,在几个架构和编译器中实现。


@user1548637 我认为他的意思是使用存储在数组中的所有参数调用函数。类似于 call_func(func_pointer, array_of_args); 这样的代码将会调用 func_name(int arg1, float arg2, char *arg3) 函数。 - Vlad Balmos
2
像Ruby和Python这样的语言不需要在C中实现此功能,只需在解释语言中以这种方式公开即可。在Ruby中(至少是基于C的MRI),参数作为指向Ruby“VALUE”的指针数组或作为单个“VALUE”传递,该“VALUE”是Ruby数组对象。请参见此处:https://github.com/ruby/ruby/blob/trunk/README.EXT#L317我认为(C)Python的实现类似。 - John Ledbetter
@AnttiHaapala:问题中的实现看起来完全可移植。你为什么会这么说? - Michael Foukarakis
@StephenGarle 谢谢。我知道其他语言也可以做到这种事情。我只是标记了Ruby和Python来吸引实现者黑客回答C语言,以及它们的内部如何完成这项工作。 - oblitum
@IgnacioVazquez-Abrams 我没有直接比较语言。 - oblitum
显示剩余3条评论
4个回答

12
您询问如何以可移植的方式调用给定参数数量的任何函数指针。正确的答案是不存在这样的方法。
例如,Python可以通过ctypes模块调用C函数,但只要您知道确切的原型和调用约定,这种方法才是可移植的。在C中,实现相同功能的最简单方式是在编译时了解函数指针的原型。
更新: 对于Python/ctypes示例,在启用了ctypes模块的每个平台上,Python都知道如何为给定的一组参数编写调用堆栈。例如,在Windows上,Python知道2种标准调用约定——带有C参数顺序的cdecl和“Pascal风格排序”的stdcall。在Linux上,它需要考虑调用32位还是64位共享对象等问题。如果将Python编译到另一个平台,则ctypes也需要进行更改;ctypes模块中的C代码并不具备可移植性。
更新2: 对于Python,其神奇之处在于此:ctypes源代码。值得注意的是,它似乎链接了http://sourceware.org/libffi/,这可能正是您所需要的。

当然可以。实质上,你只需要知道C编译器为调用给定格式的函数在特定平台上生成的代码类型即可。但是这种代码并不具备可移植性,因为C程序可能无法在符合当前C标准的所有未来系统上运行。 - Antti Haapala -- Слава Україні
Antti,谢谢,我只是还没有看过Python、Ruby或其他代码库。我的问题只是想要一些链接和示例代码,以证明你所断言的内容,因为没有通用的形式。 - oblitum
1
那么我的建议是你阅读ctypes模块的C源代码 :) - Antti Haapala -- Слава Україні
1
@Chico,好的,所以你想知道ctypes、SWIG(或其他工具)如何实现它们的抽象层。我不确定的是,为什么你认为这可以以可移植的方式完成。所有这些工具都有特定于平台的实现,有时它们的抽象不能完全统一。 - Bruno
@Bruno,我提供了一个看起来很便携的示例,某些脚本语言的实现是否可以使用类似于这样的东西,但有一些限制,例如30个参数,一个巨大的开关?它是可以的。我已经在问题中放置了所有要求。我正在寻找一种“已知”的方法,如果有的话。我不仅仅是询问可移植的方式。 - oblitum
显示剩余9条评论

8

我是libffi的作者。它可以做你所要求的事情。


7

@AnttiHaapala指出了libffi。以下是关于它的一些信息:

什么是libffi?

有些程序在编译时可能不知道要传递给函数的参数。例如,解释器可能在运行时告诉有关调用特定函数所使用的参数的数量和类型。在这种程序中可以使用'libffi'提供从解释器程序到编译代码的桥梁。

'libffi'库提供了一个可移植的、高级的编程接口,用于各种调用约定。这允许程序员在运行时调用任何由调用接口描述指定的函数。

FFI代表Foreign Function Interface。外部函数接口是允许用一种语言编写的代码调用用另一种语言编写的代码的流行名称。'libffi'库实际上只提供了完全功能的外部函数接口的最低机器相关层。必须存在一个在'libffi'上面的层,处理在两种语言之间传递的值的类型转换。

'libffi'假定您有一个指向要调用的函数的指针,并且您知道要传递的参数的数量和类型,以及函数的返回类型。


历史背景

libffi最初由Anthony Green(SO用户:anthony-green)开发,受Silicon Graphics的Gencall库启发。 Gencall是由当时在SGI工作的Gianni Mariani开发的,目的是允许通过地址调用函数并为特定的调用约定创建调用帧。Anthony Green改进了这个想法,并将其扩展到其他架构和调用约定,并开源了libffi。


使用libffi调用pow函数

#include <stdio.h>
#include <math.h>
#include <ffi.h>

int main()
{
  ffi_cif     call_interface;
  ffi_type    *ret_type;
  ffi_type    *arg_types[2];

  /* pow signature */
  ret_type = &ffi_type_double;
  arg_types[0] = &ffi_type_double;
  arg_types[1] = &ffi_type_double;

  /* prepare pow function call interface */
  if (ffi_prep_cif(&call_interface, FFI_DEFAULT_ABI, 2, ret_type, arg_types) == FFI_OK)
  {
    void *arg_values[2];
    double x, y, z;

    /* z stores the return */
    z = 0;

    /* arg_values elements point to actual arguments */
    arg_values[0] = &x;
    arg_values[1] = &y;

    x = 2;
    y = 3;

    /* call pow */
    ffi_call(&call_interface, FFI_FN(pow), &z, arg_values);

    /* 2^3=8 */
    printf("%.0f^%.0f=%.0f\n", x, y, z);
  }

  return 0;
}

我认为我可以断言libffi是一种可移植的方法来完成我所要求的事情,与Antti Haapala声称没有这样的方法相反。如果我们不能将libffi称为一种可移植技术,考虑到它被移植/实现到了许多编译器和架构,并且其接口符合C标准,那么我们也无法将C或其他任何东西称为可移植的。

信息和历史记录提取自:

https://github.com/atgreen/libffi/blob/master/doc/libffi.info

http://en.wikipedia.org/wiki/Libffi


2

为了安全起见,应在发送变量之前对其进行解包。使用汇编语言来突破参数堆栈可能在编译器之间不可移植。调用约定可能会有所不同。

我不能代表Ruby,但我已经写过很多使用C接口到Perl和Python的程序。Perl和Python变量与C变量不直接可比较,它们具有更多功能。例如,Perl标量可能具有双字符串和数字值,其中只有一个在任何时候都是有效的。

Perl/Python变量与C之间的转换是使用packunpack(在Python中的struct模块)进行的。在C接口上,您必须调用特定的API来执行转换,具体取决于类型。因此,这不仅仅是直接指针传递,而且肯定不涉及汇编语言。


看起来你在谈论从C调用脚本。我说的是相反的,即从脚本语言调用C API,比如在Python中使用ctypes。 - oblitum
我尝试同时包含两者。一个用C语言编写的Python模块具有以下原型:PyObject * func-name(PyObject *self, PyObject *args),但这还不够。您还必须定义调用约定。 - cdarke
1
@Chico:这个调用不是脚本和C之间的直接调用,而是解释器执行调用。在Perl中有XSLoader,在Python中python运行时处理调用。我同意pack/unpack可能简化了该过程。 - cdarke
@Chico:据我所知,这些参数列表是不灵活的,但可以通过包装器完成,在编译时定义与解释器的链接。如果你想要一个灵活的列表,请看一下手动动态链接(不确定你具体想做什么)。 - Bruno
1
@Chico:好的,完成转换后,它将在参数栈上传递指针,指针将被复制,但不包括值。如果调用是可变参数的,则第一个参数将指示随后参数的大小和数量(类似于printf),这就是为什么C调用约定(cdecl)是从右到左的原因,因此在被调用函数中,堆栈顶部(Top-Of-Stack)指的是传递的第一个参数。 - cdarke
显示剩余4条评论

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