继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

在 C 语言中调用不定参数的外部函数

隔江千里
关注TA
已关注
手记 165
粉丝 37
获赞 179

前言

我曾经一直有个困惑,就是像 JavaScriptPython 这样的脚本语言,是如何做到调用一个外部声明的 native 函数的呢?

试想有一个动态链接库,里面有一个两个参数的函数。如果说我们在 C 语言中调用,无非就是先用 dlopen 打开动态链接库,然后用 dlsym 拿到函数的地址,然后强制转换到预先声明的一个函数签名,然后就可以直接像调用本地函数一样调用它了。但是,如果我们在 Python 中调用,我们用 ctypes.CDLL 打开一个动态链接库,然后直接就可以调用其中的任意参数了,那么 Python 运行时是怎么处理参数列表和返回值的问题的呢?解析函数地址固然简单,但是显然 Python 不可能为许多不可预知的函数声明一堆函数签名。于是我便开始研究这其中的奥秘。

探究的开始

因为我一直在做 iOS 开发,对于 Objective-C Runtime 也有一定的了解,其实 OC 底层在调用一个类实例的方法时采用了发送消息的方式。例如:

[aObject someMethodWithArg1:foo arg2:bar];

在编译时将会自动转换为纯 C 语言调用:

objc_msgSend(aObject, @selector(someMethodWithArg1:arg2:), foo, bar);

然后函数内部会根据 SEL,在 id 所表示的类继承链里寻找相应的 IMP,当然内部还会做一些动态解析和消息转发的工作,与本文无关,这里就不赘述了。但是重点是在找到 IMP 后,怎么去调用它。这里苹果所采用的方式比较取巧,那就是用 Assembly(汇编) 实现,因为函数在被调用之前,会有一个准备工作(称之为 Prologue),在这期间,函数所需的参数放到寄存器、栈上,然后直接 calljmp 到指定的地址即可。因此使用汇编能拥有对栈帧的完全控制,另一方面也能提升性能。

然而 Python 看起来完全不是这么干的,它也没必要这么干,来看看一个外部函数在被调用时经历了怎样的一个过程:


https://img.mukewang.com/5d5812640001104f07950322.png

NSLog 在 Python 中的函数调用栈

看到高亮的那行了吗?这就是奥秘所在。来看看 Python 源码中这个调用的过程:

PyObject *_ctypes_callproc(PPROC pProc,
                    PyObject *argtuple,
#ifdef MS_WIN32
                    IUnknown *pIunk,
                    GUID *iid,
#endif                    int flags,
                    PyObject *argtypes, /* misleading name: This is a tuple of
                                           methods, not types: the .from_param
                                           class methods of the types */
            PyObject *restype,
            PyObject *checker)
{
    Py_ssize_t i, n, argcount, argtype_count;    void *resbuf;    struct argument *args, *pa;
    ffi_type **atypes;
    ffi_type *rtype;    void **avalues;
    PyObject *retval = NULL;

    n = argcount = PyTuple_GET_SIZE(argtuple);#ifdef MS_WIN32
    /* an optional COM object this pointer */
    if (pIunk)
        ++argcount;#endif

    // ...

    if (-1 == _call_function_pointer(flags, pProc, avalues, atypes,
                                     rtype, resbuf,
                                     Py_SAFE_DOWNCAST(argcount,
                                                      Py_ssize_t,                                                      int)))        goto cleanup;    // ...}

很明显,Python 在处理外部函数调用时用到了 libffi,在这个函数中最重要的就是 _call_function_pointer 这个函数调用,我们接着往下看:

static int _call_function_pointer(int flags,
                                  PPROC pProc,                                  void **avalues,
                                  ffi_type **atypes,
                                  ffi_type *restype,                                  void *resmem,                                  int argcount)
{#ifdef WITH_THREAD
    PyThreadState *_save = NULL; /* For Py_BLOCK_THREADS and Py_UNBLOCK_THREADS */#endif
    PyObject *error_object = NULL;    int *space;
    ffi_cif cif;    int cc;#ifdef MS_WIN32
    int delta;#ifndef DONT_USE_SEH
    DWORD dwExceptionCode = 0;
    EXCEPTION_RECORD record;#endif#endif
    /* XXX check before here */
    if (restype == NULL) {
        PyErr_SetString(PyExc_RuntimeError,                        "No ffi_type for result");        return -1;
    }

    cc = FFI_DEFAULT_ABI;#if defined(MS_WIN32) && !defined(MS_WIN64) && !defined(_WIN32_WCE)
    if ((flags & FUNCFLAG_CDECL) == 0)
        cc = FFI_STDCALL;#endif
    if (FFI_OK != ffi_prep_cif(&cif,
                               cc,
                               argcount,
                               restype,
                               atypes)) {
        PyErr_SetString(PyExc_RuntimeError,                        "ffi_prep_cif failed");        return -1;
    }    if (flags & (FUNCFLAG_USE_ERRNO | FUNCFLAG_USE_LASTERROR)) {
        error_object = _ctypes_get_errobj(&space);        if (error_object == NULL)            return -1;
    }#ifdef WITH_THREAD
    if ((flags & FUNCFLAG_PYTHONAPI) == 0)
        Py_UNBLOCK_THREADS#endif
    if (flags & FUNCFLAG_USE_ERRNO) {        int temp = space[0];
        space[0] = errno;
        errno = temp;
    }#ifdef MS_WIN32
    if (flags & FUNCFLAG_USE_LASTERROR) {        int temp = space[1];
        space[1] = GetLastError();
        SetLastError(temp);
    }#ifndef DONT_USE_SEH
    __try {#endif
        delta =
#endif
                ffi_call(&cif, (void *)pProc, resmem, avalues);    // ...}

经过从 Python Object 层面到 C 语言层面的一个 Bridge 过程之后,ffi_call 所需的所有环境都创建完毕,代码片段的最后一行,完美实现函数调用。

What's the Hell?

说了这么多,libffi 到底是什么?我 Google 了一下,有这样一篇文章描述地很清晰:https://img2.mukewang.com/5d58126e00012f4306480172.png也就是说,只要你知道函数的参数类型和参数个数以及返回值的类型,你就可以不用函数签名来间接调用这个函数,我想其内部实现应该和 OC 底层相似。

谜底揭开

OK,到这我们来尝试一下这个库,用它来调用一个函数,而不使用函数签名。

首先我先声明一个简单的函数,作用就是用两个参数进行幂计算并用结果生成字符串:

char *exp_string(double b, int n) {    double result = 1;    for (int i = 0; i < n; i++) {
        result *= b;
    }    
    char *str = (char *) malloc(sizeof(char) * 50);    snprintf(str, 50, "%f", result);    
    return str;
}

很简单,然后我们用 libffi 调用它:

int main(int argc, char *argv[]) {
    ffi_cif cif;    // 函数调用所需的上下文
    
    ffi_type *arg_types[2];    // 参数类型指针数组
    void *arg_values[2];    // 参数值指针数组
    ffi_status status;    
    // 根据被调用函数的参数类型进行设定.
    arg_types[0] = &ffi_type_double;
    arg_types[1] = &ffi_type_sint32;    
    // 这里 ffi_prep_cif 的第三个参数为被调用函数参数数量, 第四个参数为返回值类型的指针.
    if ((status = ffi_prep_cif(&cif, FFI_UNIX64, 2, &ffi_type_pointer, arg_types)) != FFI_OK) {
        perror("ffi_prep_cif");        abort();
    }    
    // 设置函数参数.
    double arg_b = 3.14;    int arg_n = 6;
    
    arg_values[0] = &arg_b;
    arg_values[1] = &arg_n;    
    // 声明返回值存放的变量.
    char *retVal;    
    // 交给 libffi 调用这个函数.
    ffi_call(&cif, FFI_FN(exp_string), &retVal, arg_values);    
    // 输出结果.
    printf("Function result: %s\n", retVal);    
    return 0;
}

其实就是简单设置一下上下文,就可以直接拿去给库调用了,很简单。我们看看调用结果:

https://img.mukewang.com/5d5812770001517c03580130.png结果符合我们的预期,效果和直接调用函数一致。

Wrap Up

有了 libffi,我们就不用操心汇编层面的栈帧、寄存器的维护了,直接去做我们业务逻辑就可以了。当然,我们还可以把这个库进行简单的封装,例如用 Type Encoding 的方式将类型进行统一的编码,一起放到函数名字符串中,然后用 VA_LIST 来传递参数,我们就有望把上面如此繁琐的步骤变成下面这样了:

char *result = dylib_call("libexample.dylib", "@$exp_string$di", 3.14, 6);

是不是十分方便呢,当然,这个封装我还没有写呢...

所以,有时候系统底层的东西也十分有意思,这就是为什么搞应用时间长了,老想做点别的,因为你了解的越多,眼界和经验也就越广阔,越丰富,知识需要不断的积累,而这个过程就是我们不断探索未知的过程。



作者:Cyandev
链接:https://www.jianshu.com/p/eb4fe09903fb

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP