【libffi】動態(tài)調(diào)用&定義C函數(shù)

圖片發(fā)自簡書App

這段時間在做一個組件開發(fā),要實現(xiàn)JS那邊動態(tài)調(diào)用一個含有block參數(shù)的OC方法,接觸到了libffi,主要涉及使用libffi 動態(tài)調(diào)用和定義C函數(shù)兩個方面,下面是使用之后的一些總結(jié)。參考了bang的博客這里

一、Calling Convention

高級語言編譯器將代碼編譯成相應(yīng)匯編指令時都會依據(jù)一系列的規(guī)則,這些規(guī)則十分必要,特別是對獨立編譯來說。其中之一是“調(diào)用約定” (Calling Convention),它包含了編譯器關(guān)于函數(shù)入口處的函數(shù)參數(shù)、函數(shù)返回值的一系列假設(shè)。它有時也被稱作“ABI”(Application Binary Interface)。調(diào)用約定(Calling Conventions)定義了程序中調(diào)用函數(shù)的方式,它決定了在函數(shù)調(diào)用的時候數(shù)據(jù)(比如說參數(shù))在堆棧中的組織方式。

編譯器按照調(diào)用規(guī)則去編譯,把數(shù)據(jù)放到相應(yīng)的堆棧中,那么意味著函數(shù)的調(diào)用方和被調(diào)用方(函數(shù)本身)也要遵循這個統(tǒng)一的約定,不然函數(shù)執(zhí)行過程中,會因為取不到相應(yīng)類型參數(shù)和無法正確返回而崩潰。

下面看個例子:

int testFunc(int n, int m) {
  return n+m;
}

int main() {
  // (1)
  int (*funcPointer)(int, int) = dlsym(RTLD_DEFAULT, "testFunc");
  funcPointer(1, 2);

  // (2)
  void (*funcPointer)() = dlsym(RTLD_DEFAULT, "testFunc");
  funcPointer(1, 2); //error

  return 0;
}

運行上面代碼發(fā)現(xiàn),(1)處正常執(zhí)行,(2)處崩潰,原因就是因為(2)處funcPointer的定義沒有遵循調(diào)用規(guī)則,和原函數(shù)本身的定義不符,而編譯器編譯的時候認(rèn)為funcPointer是個無參數(shù)函數(shù),則不會在執(zhí)行棧上分配兩個int型的內(nèi)存空間用來存儲實參1,2;這樣當(dāng)exp指針跳轉(zhuǎn)到代碼區(qū)執(zhí)行原函數(shù),去取對應(yīng)參數(shù)時,會出現(xiàn)取不到或取到的內(nèi)容有誤的情況,從而導(dǎo)致崩潰。

可見我們在函數(shù)調(diào)用前,需要明確的告訴編譯器這個函數(shù)的參數(shù)和返回值類型是什么,函數(shù)才能正常執(zhí)行。

那這樣來說動態(tài)的調(diào)用一個C函數(shù)是不可能實現(xiàn)了,因為我們在編譯前,就要將遵循調(diào)用規(guī)則的函數(shù)調(diào)用寫在需要調(diào)用的地方,然后通過編譯器編譯生成對應(yīng)的匯編代碼,將相應(yīng)的棧和寄存器狀態(tài)準(zhǔn)備好。如果想在運行時動態(tài)去調(diào)用的話,將沒有人為我們做這一系列的處理。

所以我們要解決的問題是:當(dāng)我們在運行時動態(tài)調(diào)用一個函數(shù)時,自己要先將相應(yīng)棧和寄存器狀態(tài)準(zhǔn)備好,然后生成相應(yīng)的匯編指令。這也正是libffi所做的。

二、libffi

FFI(Foreign Function Interface)允許以一種語言編寫的代碼調(diào)用另一種語言的代碼,而libffi庫提供了最底層的、與架構(gòu)相關(guān)的、完整的FFI。libffi的作用就相當(dāng)于編譯器,它為多種調(diào)用規(guī)則提供了一系列高級語言編程接口,然后通過相應(yīng)接口完成函數(shù)調(diào)用,底層會根據(jù)對應(yīng)的規(guī)則,完成數(shù)據(jù)準(zhǔn)備,生成相應(yīng)的匯編指令代碼。

那么這樣我們就可以通過libffi動態(tài)的調(diào)用任意C函數(shù),那libffi 具體怎么使用呢?詳細(xì)文檔請看:這里

三、動態(tài)調(diào)用C函數(shù)

使用libffi提供接口動態(tài)調(diào)用流程如下:

  1. 準(zhǔn)備好參數(shù)數(shù)據(jù)及其對應(yīng)ffi_type數(shù)組、返回值內(nèi)存指針、函數(shù)指針
  2. 創(chuàng)建與函數(shù)特征相匹配的函數(shù)原型:ffi_cif對象
  3. 使用“ffi_call”來完成函數(shù)調(diào)用

需使用的libffi API:

/* 封裝函數(shù)原型
ffi_prep_cif returns a libffi status code, of type ffi_status. This will be either FFI_OK if everything worked properly; FFI_BAD_TYPEDEF if one of the ffi_type objects is incorrect; or FFI_BAD_ABI if the abi parameter is invalid.
*/
ffi_status ffi_prep_cif(ffi_cif *cif,
            ffi_abi abi,                  //abi is the ABI to use; normally FFI_DEFAULT_ABI is what you want. Multiple ABIs for more information.
            unsigned int nargs,           //nargs is the number of arguments that this function accepts. ‘libffi’ does not yet handle varargs functions; see Missing Features for more information.
            ffi_type *rtype,              //rtype is a pointer to an ffi_type structure that describes the return type of the function. See Types.
            ffi_type **atypes);           //argtypes is a vector of ffi_type pointers. argtypes must have nargs elements. If nargs is 0, this argument is ignored.
    
/*  調(diào)用指定函數(shù)
This calls the function fn according to the description given in cif. cif must have already been prepared using ffi_prep_cif.
*/
void ffi_call(ffi_cif *cif,
          void (*fn)(void),
          void *rvalue,                   //rvalue is a pointer to a chunk of memory that will hold the result of the function call. This must be large enough to hold the result and must be suitably aligned; it is the caller's responsibility to ensure this. If cif declares that the function returns void (using ffi_type_void), then rvalue is ignored. If rvalue is ‘NULL’, then the return value is discarded.
          void **avalue);                 //avalues is a vector of void * pointers that point to the memory locations holding the argument values for a call. If cif declares that the function has no arguments (i.e., nargs was 0), then avalues is ignored. Note that argument values may be modified by the callee (for instance, structs passed by value); the burden of copying pass-by-value arguments is placed on the caller.

下面看一個簡單的例子:

int testFunc(int m, int n) {
    printf("params: %d %d \n", m, n);
    return m+n;
}

+ (void)testCall {
    testFunc(1, 2);
    
    //拿函數(shù)指針
    void* functionPtr = &testFunc;
    int argCount = 2;
    
    //參數(shù)類型數(shù)組
    ffi_type **ffiArgTypes = alloca(sizeof(ffi_type *) *argCount);
    ffiArgTypes[0] = &ffi_type_sint;
    ffiArgTypes[1] = &ffi_type_sint;
    
    //參數(shù)數(shù)據(jù)數(shù)組
    void **ffiArgs = alloca(sizeof(void *) *argCount);
    void *ffiArgPtr = alloca(ffiArgTypes[0]->size);
    int *argPtr = ffiArgPtr;
    *argPtr = 5;
    ffiArgs[0] = ffiArgPtr;
    
    void *ffiArgPtr2 = alloca(ffiArgTypes[1]->size);
    int *argPtr2 = ffiArgPtr2;
    *argPtr2 = 3;
    ffiArgs[1] = ffiArgPtr2;
    
    //生成函數(shù)原型 ffi_cfi 對象
    ffi_cif cif;
    ffi_type *returnFfiType = &ffi_type_sint;
    ffi_status ffiPrepStatus = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, (unsigned int)argCount, returnFfiType, ffiArgTypes);
    
    if (ffiPrepStatus == FFI_OK) {
        //生成用于保存返回值的內(nèi)存
        void *returnPtr = NULL;
        if (returnFfiType->size) {
            returnPtr = alloca(returnFfiType->size);
        }
        //根據(jù)cif函數(shù)原型,函數(shù)指針,返回值內(nèi)存指針,函數(shù)參數(shù)數(shù)據(jù)調(diào)用這個函數(shù)
        ffi_call(&cif, functionPtr, returnPtr, ffiArgs);
        
        //拿到返回值
        int returnValue = *(int *)returnPtr;
        printf("ret: %d \n", returnValue);
    }
}

執(zhí)行結(jié)果:

params: 1 2 
params: 5 3 
ret: 8 

可見使用ffi,只要有函數(shù)原型cif對象,函數(shù)實現(xiàn)指針,返回值內(nèi)存指針和函數(shù)參數(shù)數(shù)組,我們就可以實現(xiàn)在運行時動態(tài)調(diào)用任意C函數(shù)。

所以如果想實現(xiàn)其他語言(譬如JS),執(zhí)行過程中動態(tài)調(diào)用C函數(shù),只需在調(diào)用過程中加一層轉(zhuǎn)換,將參數(shù)及返回值類型轉(zhuǎn)換成libffi對應(yīng)類型,并封裝成函數(shù)原型cif對象,準(zhǔn)備好參數(shù)數(shù)據(jù),找到對應(yīng)函數(shù)指針,然后調(diào)用即可。

四、動態(tài)定義C函數(shù)

libffi還有一個特別強大的函數(shù),通過它我們可以將任意參數(shù)和返回值類型的函數(shù)指針,綁定到一個函數(shù)實體上。那么這樣我們就可以很方便的實現(xiàn)動態(tài)定義一個C函數(shù)了!同時這個函數(shù)在編寫解釋器或提供任意函數(shù)的包裝器(通用block)時非常有用,此函數(shù)是:

ffi_status ffi_prep_closure_loc (ffi_closure *closure,  //閉包,一個ffi_closure對象
       ffi_cif *cif,  //函數(shù)原型
       void (*fun) (ffi_cif *cif, void *ret, void **args, void*user_data), //函數(shù)實體
       void *user_data, //函數(shù)上下文,函數(shù)實體實參
       void *codeloc)   //函數(shù)指針,指向函數(shù)實體

來看下函數(shù)各參數(shù)詳細(xì)說明:

Prepare a closure function.

參數(shù) closure is the address of a ffi_closure object; this is the writable address returned by ffi_closure_alloc.

參數(shù) cif is the ffi_cif describing the function parameters.

參數(shù) user_data is an arbitrary datum that is passed, uninterpreted, to your closure function.

參數(shù) codeloc is the executable address returned by ffi_closure_alloc.

函數(shù)實體 fun is the function which will be called when the closure is invoked. It is called with the arguments:

函數(shù)實體參數(shù) cif
The ffi_cif passed to ffi_prep_closure_loc. 
函數(shù)實體參數(shù) ret
A pointer to the memory used for the function's return value. fun must fill this, unless the function is declared as returning void. 
函數(shù)實體參數(shù) args
A vector of pointers to memory holding the arguments to the function. 
函數(shù)實體參數(shù) user_data
The same user_data that was passed to ffi_prep_closure_loc.
ffi_prep_closure_loc will return FFI_OK if everything went ok, and something else on error.

After calling ffi_prep_closure_loc, you can cast codeloc to the appropriate pointer-to-function type.

You may see old code referring to ffi_prep_closure. This function is deprecated, as it cannot handle the need for separate writable and executable addresses.

下面通過一個簡單的例子,看下如何將一個函數(shù)指針綁定到一個函數(shù)實體上:

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

/* Acts like puts with the file given at time of enclosure. */
// 函數(shù)實體
void puts_binding(ffi_cif *cif, unsigned int *ret, void* args[],
                  FILE *stream)
{
    *ret = fputs(*(char **)args[0], stream);
}

int main()
{
    ffi_cif cif;
    ffi_type *args[1];
    ffi_closure *closure;
    
    int (*bound_puts)(char *);  //聲明一個函數(shù)指針
    int rc;
    
    /* Allocate closure and bound_puts */  //創(chuàng)建closure
    closure = ffi_closure_alloc(sizeof(ffi_closure), &bound_puts);
    
    if (closure)
    {
        /* Initialize the argument info vectors */
        args[0] = &ffi_type_pointer;
        
        /* Initialize the cif */  //生成函數(shù)原型
        if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1,
                         &ffi_type_uint, args) == FFI_OK)
        {
            /* Initialize the closure, setting stream to stdout */
            // 通過 ffi_closure 把 函數(shù)原型_cifPtr / 函數(shù)實體JPBlockInterpreter / 上下文對象self / 函數(shù)指針blockImp 關(guān)聯(lián)起來
            if (ffi_prep_closure_loc(closure, &cif, puts_binding,
                                     stdout, bound_puts) == FFI_OK)
            {
                rc = bound_puts("Hello World!");
                /* rc now holds the result of the call to fputs */
            }
        }
    }
    
    /* Deallocate both closure, and bound_puts */
    ffi_closure_free(closure);   //釋放閉包
    
    return 0;
}

上述步驟大致分為:

  1. 準(zhǔn)備一個函數(shù)實體
  2. 聲明一個函數(shù)指針
  3. 根據(jù)函數(shù)參數(shù)個數(shù)/參數(shù)及返回值類型生成一個函數(shù)原型
  4. 創(chuàng)建一個ffi_closure對象,并用其將函數(shù)原型、函數(shù)實體、函數(shù)上下文、函數(shù)指針關(guān)聯(lián)起來
  5. 釋放closure

通過以上這5步,我們就可以在執(zhí)行過程中將一個函數(shù)指針,綁定到一個函數(shù)實體上,從而輕而易舉的實現(xiàn)動態(tài)定義一個C函數(shù)。

由上可知:如果我們利用好user_data,用其傳入我們想要的函數(shù)實現(xiàn),將函數(shù)實體變成一個通用的函數(shù)實體,然后將函數(shù)指針改為void,通過結(jié)構(gòu)體創(chuàng)建一個block保存函數(shù)指針并返回,那么我們就可以實現(xiàn)JS調(diào)用含有任意類型block參數(shù)的OC方法了(后續(xù)文章會簡要概述說明)*

到這我們已經(jīng)清楚的了解了libffi的秒用,以后實際應(yīng)用中,我們可以利用它輕松實現(xiàn)多種語言之間的互相調(diào)用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,672評論 1 51
  • 原文地址:C語言函數(shù)調(diào)用棧(一)C語言函數(shù)調(diào)用棧(二) 0 引言 程序的執(zhí)行過程可看作連續(xù)的函數(shù)調(diào)用。當(dāng)一個函數(shù)執(zhí)...
    小豬啊嗚閱讀 4,971評論 1 19
  • 重新系統(tǒng)學(xué)習(xí)下C++;但是還是少了好多知識點;socket;unix;stl;boost等; C++ 教程 | 菜...
    kakukeme閱讀 20,454評論 0 50
  • 題目類型 a.C++與C差異(1-18) 1.C和C++中struct有什么區(qū)別? C沒有Protection行為...
    阿面a閱讀 7,891評論 0 10
  • __block和__weak修飾符的區(qū)別其實是挺明顯的:1.__block不管是ARC還是MRC模式下都可以使用,...
    LZM輪回閱讀 3,594評論 0 6

友情鏈接更多精彩內(nèi)容