在 C 中,给定参数的变量列表,如何使用它们构建函数调用?

Posted

技术标签:

【中文标题】在 C 中,给定参数的变量列表,如何使用它们构建函数调用?【英文标题】:In C, given a variable list of arguments, how to build a function call using them? 【发布时间】:2012-08-29 03:05:07 【问题描述】:

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

给定一个函数指针,我如何通过存储的参数列表对其进行调用?

我没有尝试将数组作为参数传递。你明白了,好吗?我想将它的每个元素作为参数传递。数组只是为了说明,我可以将参数存储在一些元组结构中。另外,请看我手头有一个函数指针,并且可能有一个字符串格式的签名。我不想仅仅定义一个能够处理可变参数列表的函数。

我知道如何做到这一点的唯一方法是使用汇编(__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 函数,其指针和签名仅在运行时可用?

更新

来自Foreign Interface for PLT Scheme:

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

感谢@AnttiHaapala 搜索、查找和指向libffi。这是我一直在寻找的,它被许多脚本语言使用,它是一个可移植的库,跨多个架构和编译器实现。

【问题讨论】:

@user1548637 我认为他的意思是使用存储在数组中的所有参数调用函数。像call_func(func_pointer, array_of_args); 这样的东西会调用func_name(int arg1, float arg2, char *arg3) 像 Ruby 和 Python 这样的语言不需要在 C 中实现此功能,只需在解释语言中以这种方式公开即可。在 Ruby(至少基于 C 的 MRI)中,参数作为指向 Ruby VALUEs 的指针数组或作为单个 VALUE(这是一个 Ruby 数组对象)传递。见这里:github.com/ruby/ruby/blob/trunk/README.EXT#L317 我假设在 (C)Python 中的实现是相似的。 你问“什么是便携的通话方式”,正确答案是:没有。 @AnttiHaapala:问题中的实现似乎完全可移植。为什么会这样说? 我看到你已经标记了 ruby​​,即使问题是针对 C 的。如果你很好奇,ruby 有用于将它们转换为参数的数组的 splat 运算符。如果你有一个包含 2 个元素的数组和一个带有两个参数的函数,你可以说 func(*a)。 【参考方案1】:

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

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

Perl/Python 变量和 C 之间的转换是使用 packunpack(在 Python 中的 struct 模块中)完成的。在 C 接口中,您必须调用特定的 API 来进行转换,具体取决于类型。所以,这不仅仅是直接的指针传输,当然也不涉及汇编。

【讨论】:

看来你说的是从 C 调用脚本。我说的是相反的,从脚本语言调用 C API,就像在 python 中使用 ctypes。 我尝试将两者都包含在内。用 C 语言编写的 Python 模块具有以下原型:PyObject * func-name(PyObject *self, PyObject *args),但它的功能远不止这些。您还必须定义调用约定。 ctypes 添加了一个包装器,用于将 Python 对象转换为普通的 C 变量,这将涉及调用时的 pack 操作和返回时的 unpack @Chico:调用不是脚本和C之间的直接调用,解释器执行调用。在 Perl 中有 XSLoader,在 Python 中,python 运行时处理调用。我同意pack/unpack 可能正在简化程序。 @Chico:好的,完成转换后,它将在参数堆栈上传递指针,指针将被复制,但值不会被复制。如果调用是可变参数,那么第一个参数将指示后面参数的大小和数量(想想printf),这就是为什么 C 调用约定(cdecl)是从右到左的,所以栈顶(在被调用函数) 指的是传递的第一个参数。【参考方案2】:

你问用给定数量的参数调用 any 函数指针的可移植方式是什么。正确的答案是没有这种方法

例如,python 可以通过 ctypes 模块调用 C 函数,但这仅在您知道确切的原型和调用约定的情况下是可移植的。在 C 中实现相同的最简单方法是在编译时知道函数指针的原型。

更新

对于 python / ctypes 示例,在启用 ctypes 模块的每个平台上,python 知道如何为给定的一组参数编写调用堆栈。例如,在 Windows 上,python 知道 2 个标准调用约定 - cdecl 具有 C 堆栈上的参数顺序,而 stdcall 具有“pascal 样式排序”。在 Linux 上,它确实需要担心是调用 32 位还是 64 位共享对象等等。如果 python 编译到另一个平台,ctypes 也需要更改; ctypes 模块中的 C 代码本身不是可移植的。

更新 2

对于 Python,魔力就在这里:ctypes source code。值得注意的是,它似乎链接了http://sourceware.org/libffi/,这可能正是您所需要的。

【讨论】:

问题表明我可以在运行时将原型设为字符串格式。 当然可以,本质上您只需要确切地知道 C 编译器会为确切的此类平台生成什么样的代码,以调用具有给定格式的函数。然而,这样的代码不能移植,因为 C 程序可以在符合当前 C 标准的每个未来系统上运行。 如果不能便携,那么下面的问题就来了。脚本语言是如何做到这一点的? 那我的建议是你阅读ctypes模块C源码:) @Chico,很公平,所以你想知道ctypes、SWIG(或其他)之类的人如何实现它们的抽象层。我不确定的是为什么你认为这可以以便携的方式完成。所有这些工具都有特定于平台的实现,有时它们的抽象不能完全统一。【参考方案3】:

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

什么是 libffi?

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

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

FFI 代表外部功能接口。外来函数接口是接口的通称,它允许用一种语言编写的代码调用用另一种语言编写的代码。 “libffi”库实际上只提供了功能齐全的外来函数接口的最低、机器相关层。 “libffi”之上必须存在一个层,用于处理在两种语言之间传递的值的类型转换。

‘libffi’假设你有一个指向你想要调用的函数的指针,并且你知道要传递它的参数的数量和类型,以及函数的返回类型。


历史背景

libffi 最初由 Anthony Green(SO 用户:anthony-green)开发,灵感来自 Silicon Graphics 的 Gencall 库。 Gencall 由 Gianni Mariani 开发,然后被 SGI 雇用,目的是允许按地址调用函数并为特定调用约定创建调用框架。 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

【讨论】:

【参考方案4】:

我是 libffi 的作者。它会做你所要求的。

【讨论】:

以上是关于在 C 中,给定参数的变量列表,如何使用它们构建函数调用?的主要内容,如果未能解决你的问题,请参考以下文章

C# 调用具有不同类型的相同扩展函​​数作为参数(可能是委托?)

给定传递给它的参数类型,如何确定函数参数的类型?

Linux常用Shell函数参数

Linux常用Shell函数参数

C 函数中的变量参数列表 - 如何正确遍历 arg 列表?

在 C / C++ 中打印所有环境变量