新聞中心
不同的開發(fā)語言適合不同的領(lǐng)域,例如Python適合做數(shù)據(jù)分析,C++適合做系統(tǒng)的底層開發(fā),假如它們需要用到相同功能的基礎(chǔ)組件,組件使用多種語言分別開發(fā)的話,不僅增加了開發(fā)和維護(hù)成本,而且不能確保多種語言間在處理效果上是一致的。本文以美團(tuán)搜索實(shí)際場(chǎng)景下的案例,講述在Linux系統(tǒng)下跨語言調(diào)用的實(shí)踐,即開發(fā)一次C++語言的組件,其他語言通過跨語言調(diào)用技術(shù)調(diào)用C++組件。

1 背景介紹
2 方案概述
3 實(shí)現(xiàn)詳情
- 3.1 功能代碼
- 3.2 打包發(fā)布
- 3.3 業(yè)務(wù)使用
- 3.4 易用性優(yōu)化
4. 原理介紹
- 4.1 為什么需要一個(gè)c_wrapper
- 4.2 跨語言調(diào)用如何實(shí)現(xiàn)參數(shù)傳遞
- 4.3 擴(kuò)展閱讀(JNA直接映射)
- 4.4 性能分析
5 應(yīng)用案例
- 5.1 離線任務(wù)中的應(yīng)用
- 5.2 在線服務(wù)中的應(yīng)用
6 總結(jié)
1 背景
查詢理解(QU, Query Understanding)是美團(tuán)搜索的核心模塊,主要職責(zé)是理解用戶查詢,生成查詢意圖、成分、改寫等基礎(chǔ)信號(hào),應(yīng)用于搜索的召回、排序、展示等多個(gè)環(huán)節(jié),對(duì)搜索基礎(chǔ)體驗(yàn)至關(guān)重要。該服務(wù)的線上主體程序基于C++語言開發(fā),服務(wù)中會(huì)加載大量的詞表數(shù)據(jù)、預(yù)估模型等,這些數(shù)據(jù)與模型的離線生產(chǎn)過程有很多文本解析能力需要與線上服務(wù)保持一致,從而保證效果層面的一致性,如文本歸一化、分詞等。
而這些離線生產(chǎn)過程通常用Python與Java實(shí)現(xiàn)。如果在線、離線用不同語言各自開發(fā)一份,則很難維持策略與效果上的統(tǒng)一。同時(shí)這些能力會(huì)有不斷的迭代,在這種動(dòng)態(tài)場(chǎng)景下,不斷維護(hù)多語言版本的效果打平,給我們的日常迭代帶來了極大的成本。因此,我們嘗試通過跨語言調(diào)用動(dòng)態(tài)鏈接庫的技術(shù)解決這個(gè)問題,即開發(fā)一次基于C++的so,通過不同語言的鏈接層封裝成不同語言的組件庫,并投入到對(duì)應(yīng)的生產(chǎn)過程。這種方案的優(yōu)勢(shì)非常明顯,主體的業(yè)務(wù)邏輯只需要開發(fā)一次,封裝層只需要極少量的代碼,主體業(yè)務(wù)迭代升級(jí),其它語言幾乎不需要改動(dòng),只需要包含最新的動(dòng)態(tài)鏈接庫,發(fā)布最新版本即可。同時(shí)C++作為更底層的語言,在很多場(chǎng)景下,它的計(jì)算效率更高,硬件資源利用率更高,也為我們帶來了一些性能上的優(yōu)勢(shì)。
本文對(duì)我們?cè)趯?shí)際生產(chǎn)中嘗試這一技術(shù)方案時(shí),遇到的問題與一些實(shí)踐經(jīng)驗(yàn)做了完整的梳理,希望能為大家提供一些參考或幫助。
2 方案概述
為了達(dá)到業(yè)務(wù)方開箱即用的目的,綜合考慮C++、Python、Java用戶的使用習(xí)慣,我們?cè)O(shè)計(jì)了如下的協(xié)作結(jié)構(gòu):
圖 1
3 實(shí)現(xiàn)詳情
Python、Java支持調(diào)用C接口,但不支持調(diào)用C++接口,因此對(duì)于C++語言實(shí)現(xiàn)的接口,必須轉(zhuǎn)換為C語言實(shí)現(xiàn)。為了不修改原始C++代碼,在C++接口上層用C語言進(jìn)行一次封裝,這部分代碼通常被稱為“膠水代碼”(Glue Code)。具體方案如下圖所示:
圖 2
本章節(jié)各部分內(nèi)容如下:
- 【功能代碼】部分,通過打印字符串的例子來講述各語言部分的編碼工作。
- 【打包發(fā)布】部分,介紹如何將生成的動(dòng)態(tài)庫作為資源文件與Python、Java代碼打包在一起發(fā)布到倉庫,以降低使用方的接入成本。
- 【業(yè)務(wù)使用】部分,介紹開箱即用的使用示例。
- 【易用性優(yōu)化】部分,結(jié)合實(shí)際使用中遇到的問題,講述了對(duì)于Python版本兼容,以及動(dòng)態(tài)庫依賴問題的處理方式。
3.1 功能代碼
3.1.1 C++代碼
作為示例,實(shí)現(xiàn)一個(gè)打印字符串的功能。為了模擬實(shí)際的工業(yè)場(chǎng)景,對(duì)以下代碼進(jìn)行編譯,分別生成動(dòng)態(tài)庫 libstr_print_cpp.so、靜態(tài)庫libstr_print_cpp.a。
str_print.h
#pragma once
#include
class StrPrint {
public:
void print(const std::string& text);
};
str_print.cpp
#include
#include "str_print.h"
void StrPrint::print(const std::string& text) {
std::cout << text << std::endl;
}
3.1.2 c_wrapper代碼
如上文所述,需要對(duì)C++庫進(jìn)行封裝,改造成對(duì)外提供C語言格式的接口。
c_wrapper.cpp
#include "str_print.h"
extern "C" {
void str_print(const char* text) {
StrPrint cpp_ins;
std::str
ing str = text;
cpp_ins.print(str);
}
}
3.1.3 生成動(dòng)態(tài)庫
為了支持Python與Java的跨語言調(diào)用,我們需要對(duì)封裝好的接口生成動(dòng)態(tài)庫,生成動(dòng)態(tài)庫的方式有以下三種。
方式一:源碼依賴方式,將c_wrapper和C++代碼一起編譯生成libstr_print.so。這種方式業(yè)務(wù)方只需要依賴一個(gè)so,使用成本較小,但是需要獲取到C++源碼。對(duì)于一些現(xiàn)成的動(dòng)態(tài)庫,可能不適用。
g++ -o libstr_print.so str_print.cpp c_wrapper.cpp -fPIC -shared
方式二:動(dòng)態(tài)鏈接方式,這種方式生成的libstr_print.so,發(fā)布時(shí)需要攜帶上其依賴庫libstr_print_cpp.so。業(yè)務(wù)方需要同時(shí)依賴兩個(gè)so,使用的成本相對(duì)要高,但是不必提供原動(dòng)態(tài)庫的源碼。
g++ -o libstr_print.so c_wrapper.cpp -fPIC -shared -L. -lstr_print_cpp
方式三:靜態(tài)鏈接方式,這種方式生成的libstr_print.so,發(fā)布時(shí)無需攜帶上libstr_print_cpp.so。業(yè)務(wù)方只需依賴一個(gè)so,不必依賴源碼,但是需要提供靜態(tài)庫。
g++ c_wrapper.cpp libstr_print_cpp.a -fPIC -shared -o libstr_print.so
上述三種方式,各自有適用場(chǎng)景和優(yōu)缺點(diǎn)。在我們本次的業(yè)務(wù)場(chǎng)景下,因?yàn)楣ぞ邘炫c封裝庫均由我們自己開發(fā),能夠獲取到源碼,因此選擇第一種方式,業(yè)務(wù)方依賴更加簡(jiǎn)單。
3.1.4 Python接入代碼
Python標(biāo)準(zhǔn)庫自帶的ctypes可以實(shí)現(xiàn)加載C的動(dòng)態(tài)庫的功能,使用方法如下:
str_print.py
# -*- coding: utf-8 -*-
import ctypes
# 加載 C lib
lib = ctypes.cdll.LoadLibrary("./libstr_print.so")
# 接口參數(shù)類型映射
lib.str_print.argtypes = [ctypes.c_char_p]
lib.str_print.restype = None
# 調(diào)用接口
lib.str_print('Hello World')
LoadLibrary會(huì)返回一個(gè)指向動(dòng)態(tài)庫的實(shí)例,通過它可以在Python里直接調(diào)用該庫中的函數(shù)。argtypes與restype是動(dòng)態(tài)庫中函數(shù)的參數(shù)屬性,前者是一個(gè)ctypes類型的列表或元組,用于指定動(dòng)態(tài)庫中函數(shù)接口的參數(shù)類型,后者是函數(shù)的返回類型(默認(rèn)是c_int,可以不指定,對(duì)于非c_int型需要顯示指定)。該部分涉及到的參數(shù)類型映射,以及如何向函數(shù)中傳遞struct、指針等高級(jí)類型,可以參考附錄中的文檔。
3.1.5 Java接入代碼
Java調(diào)用C lib有JNI與JNA兩種方式,從使用便捷性來看,更推薦JNA方式。
3.1.5.1 JNI接入
Java從1.1版本開始支持JNI接口協(xié)議,用于實(shí)現(xiàn)Java語言調(diào)用C/C++動(dòng)態(tài)庫。JNI方式下,前文提到的c_wrapper模塊不再適用,JNI協(xié)議本身提供了適配層的接口定義,需要按照這個(gè)定義進(jìn)行實(shí)現(xiàn)。JNI方式的具體接入步驟為:
Java代碼里,在需要跨語言調(diào)用的方法上,增加native關(guān)鍵字,用以聲明這是一個(gè)本地方法。
import java.lang.String;
public class JniDemo {
public native void print(String text);
}
通過javah命令,將代碼中的native方法生成對(duì)應(yīng)的C語言的頭文件。這個(gè)頭文件類似于前文提到的c_wrapper作用。
javah JniDemo
得到的頭文件如下(為節(jié)省篇幅,這里簡(jiǎn)化了一些注釋和宏):
#include
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_JniDemo_print
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
jni.h在JDK中提供,其中定義了Java與C語言調(diào)用所必需的相關(guān)實(shí)現(xiàn)。
JNIEXPORT和JNICALL是JNI中定義的兩個(gè)宏,JNIEXPORT標(biāo)識(shí)了支持在外部程序代碼中調(diào)用該動(dòng)態(tài)庫中的方法,JNICALL定義了函數(shù)調(diào)用時(shí)參數(shù)的入棧出棧約定。
Java_JniDemo_print是一個(gè)自動(dòng)生成的函數(shù)名,它的格式是固定,由Java_{className}_{methodName}構(gòu)成,JNI會(huì)按照這個(gè)約定去注冊(cè)Java方法與C函數(shù)的映射。
三個(gè)參數(shù)里,前兩個(gè)是固定的。JNIEnv中封裝了jni.h里的一些工具方法,jobject指向Java中的調(diào)用類,即JniDemo,通過它可以找到Java里class中的成員變量在C的堆棧中的拷貝。jstring指向傳入?yún)?shù)text,這是對(duì)于Java中String類型的一個(gè)映射。有關(guān)類型映射的具體內(nèi)容,會(huì)在后文詳細(xì)展開。
編寫實(shí)現(xiàn)Java_JniDemo_print方法。
JniDemo.cpp
#include
#include "JniDemo.h"
#include "str_print.h"
JNIEXPORT void JNICALL Java_JniDemo_print (JNIEnv *env, jobject obj, jstring text)
{
char* str=(char*)env->GetStringUTFChars(text,JNI_FALSE);
std::string tmp = str;
StrPrint ins;
ins.print(tmp);
}
編譯生成動(dòng)態(tài)庫。
g++ -o libJniDemo.so JniDemo.cpp str_print.cpp -fPIC -shared -I<$JAVA_HOME>/include/ -I<$JAVA_HOME>/include/linux
編譯運(yùn)行。
java -Djava.library.path=JniDemo
JNI機(jī)制通過一層C/C++的橋接,實(shí)現(xiàn)了跨語言調(diào)用協(xié)議。這一功能在Android系統(tǒng)中一些圖形計(jì)算相關(guān)的Java程序下有著大量應(yīng)用。一方面能夠通過Java調(diào)用大量操作系統(tǒng)底層庫,極大的減少了JDK上的驅(qū)動(dòng)開發(fā)的工作量,另一方面能夠更充分的利用硬件性能。但是通過3.1.5.1中的描述也可以看到,JNI的實(shí)現(xiàn)方式本身的實(shí)現(xiàn)成本還是比較高的。尤其橋接層的C/C++代碼的編寫,在處理復(fù)雜類型的參數(shù)傳遞時(shí),開發(fā)成本較大。為了優(yōu)化這個(gè)過程,Sun公司主導(dǎo)了JNA(Java Native Access)開源工程的工作。
3.1.5.2 JNA接入
JNA是在JNI基礎(chǔ)上實(shí)現(xiàn)的編程框架,它提供了C語言動(dòng)態(tài)轉(zhuǎn)發(fā)器,實(shí)現(xiàn)了Java類型到C類型的自動(dòng)轉(zhuǎn)換。因此,Java開發(fā)人員只要在一個(gè)Java接口中描述目標(biāo)native library的函數(shù)與結(jié)構(gòu),不再需要編寫任何Native/JNI代碼,極大的降低了Java調(diào)用本地共享庫的開發(fā)難度。
JNA的使用方法如下:
在Java項(xiàng)目中引入JNA庫。
com.sun.jna
jna
5.4.0
聲明與動(dòng)態(tài)庫對(duì)應(yīng)的Java接口類。
public interface CLibrary extends Library {
void str_print(String text); // 方法名和動(dòng)態(tài)庫接口一致,參數(shù)類型需要用Java里的類型表示,執(zhí)行時(shí)會(huì)做類型映射,原理介紹章節(jié)會(huì)有詳細(xì)解釋
}
加載動(dòng)態(tài)鏈接庫,并實(shí)現(xiàn)接口方法。
JnaDemo.java
package com.jna.demo;
import com.sun.jna.Library;
import com.sun.jna.Native;
public class JnaDemo {
private CLibrary cLibrary;
public interface CLibrary extends Library {
void str_print(String text);
}
public JnaDemo() {
cLibrary = Native.load("str_print", CLibrary.class);
}
public void str_print(String text)
{
cLibrary.str_print(text);
}
}
對(duì)比可以發(fā)現(xiàn),相比于JNI,JNA不再需要指定native關(guān)鍵字,不再需要生成JNI部分C代碼,也不再需要顯示的做參數(shù)類型轉(zhuǎn)化,極大地提高了調(diào)用動(dòng)態(tài)庫的效率。
3.2 打包發(fā)布
為了做到開箱即用,我們將動(dòng)態(tài)庫與對(duì)應(yīng)語言代碼打包在一起,并自動(dòng)準(zhǔn)備好對(duì)應(yīng)依賴環(huán)境。這樣使用方只需要安裝對(duì)應(yīng)的庫,并引入到工程中,就可以直接開始調(diào)用。這里需要解釋的是,我們沒有將so發(fā)布到運(yùn)行機(jī)器上,而是將其和接口代碼一并發(fā)布至代碼倉庫,原因是我們所開發(fā)的工具代碼可能被不同業(yè)務(wù)、不同背景(非C++)團(tuán)隊(duì)使用,不能保證各個(gè)業(yè)務(wù)方團(tuán)隊(duì)都使用統(tǒng)一的、標(biāo)準(zhǔn)化的運(yùn)行環(huán)境,無法做到so的統(tǒng)一發(fā)布、更新。
3.2.1 Python 包發(fā)布
Python可以通過setuptools將工具庫打包,發(fā)布至pypi公共倉庫中。具體操作方法如下:創(chuàng)建目錄。
.
├── MANIFEST.in #指定靜態(tài)依賴
├── setup.py # 發(fā)布配置的代碼
└── strprint # 工具庫的源碼目錄
├── __init__.py # 工具包的入口
└── libstr_print.so # 依賴的c_wrapper 動(dòng)態(tài)庫
編寫__init__.py, 將上文代碼封裝成方法。
# -*- coding: utf-8 -*-
import ctypes
import os
import sys
dirname, _ = os.path.split(os.path.abspath(__file__))
lib = ctypes.cdll.LoadLibrary(dirname + "/libstr_print.so")
lib.str_print.argtypes = [ctypes.c_char_p]
lib.str_print.restype = None
def str_print(text):
lib.str_print(text)
編寫setup.py。
from setuptools import setup, find_packages
setup(
name="strprint",
version="1.0.0",
packages=find_packages(),
include_package_data=True,
description='str print',
author='xxx',
package_data={
'strprint': ['*.so']
},
)
編寫MANIFEST.in。
include strprint/libstr_print.so
打包發(fā)布。
python setup.py sdist upload
3.2.2 Java接口
對(duì)于Java接口,將其打包成JAR包,并發(fā)布至Maven倉庫中。編寫封裝接口代碼JnaDemo.java。
package com.jna.demo;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
public class JnaDemo {
private CLibrary cLibrary;
public interface CLibrary extends Library {
Pointer create();
void str_print(String text);
}
public static JnaDemo create() {
JnaDemo jnademo = new JnaDemo();
jnademo.cLibrary = Native.load("str_print", CLibrary.class);
//System.out.println("test");
return jnademo;
}
public void print(String text)
{
cLibrary.str_print(text);
}
}
創(chuàng)建reso
創(chuàng)建resources目錄,并將依賴的動(dòng)態(tài)庫放到該目錄。通過打包插件,將依賴的庫一并打包到JAR包中。
maven-assembly-plugin
false
jar-with-dependencies
make-assembly
package
assembly
3.3 業(yè)務(wù)使用
3.3.1 Python使用
安裝strprint包。
pip install strprint==1.0.0
使用示例:
# -*- coding: utf-8 -*-
import sys
from strprint import *
str_print('Hello py')
3.3.2 Java使用
pom引入JAR包。
com.jna.demo
jnademo
1.0
使用示例:
JnaDemo jnademo = new JnaDemo();
jnademo.str_print("hello jna");
3.4 易用性優(yōu)化
3.4.1 Python版本兼容
Python2與Python3版本的問題,是Python開發(fā)用戶一直詬病的槽點(diǎn)。因?yàn)楣ぞ呙嫦虿煌臉I(yè)務(wù)團(tuán)隊(duì),我們沒有辦法強(qiáng)制要求使用統(tǒng)一的Python版本,但是我們可以通過對(duì)工具庫做一下簡(jiǎn)單處理,實(shí)現(xiàn)兩個(gè)版本的兼容。Python版本兼容里,需要注意兩方面的問題:
- 語法兼容
- 數(shù)據(jù)編碼
Python代碼的封裝里,基本不牽扯語法兼容問題,我們的工作主要集中在數(shù)據(jù)編碼問題上。由于Python 3的str類型使用的是unicode編碼,而在C中,我們需要的char* 是utf8編碼,因此需要對(duì)于傳入的字符串做utf8編碼處理,對(duì)于C語言返回的字符串,做utf8轉(zhuǎn)換成unicode的解碼處理。于是對(duì)于上例子,我們做了如下改造:
# -*- coding: utf-8 -*-
import ctypes
import os
import sys
dirname, _ = os.path.split(os.path.abspath(__file__))
lib = ctypes.cdll.LoadLibrary(dirname + "/libstr_print.so")
lib.str_print.argtypes = [ctypes.c_char_p]
lib.str_print.restype = None
def is_python3():
return sys.version_info[0] == 3
def encode_str(input):
if is_python3() and type(input) is str:
return bytes(input, encoding='utf8')
return input
def decode_str(input):
if is_python3() and type(input) is bytes:
return input.decode('utf8')
return input
def str_print(text):
lib.str_print(encode_str(text))
3.4.2 依賴管理
在很多情況下,我們調(diào)用的動(dòng)態(tài)庫,會(huì)依賴其它動(dòng)態(tài)庫,比如當(dāng)我們依賴的gcc/g++版本與運(yùn)行環(huán)境上的不一致時(shí),時(shí)常會(huì)遇到glibc_X.XX not found的問題,這時(shí)需要我們提供指定版本的libstdc.so與libstdc++.so.6。
為了實(shí)現(xiàn)開箱即用的目標(biāo),在依賴并不復(fù)雜的情況下,我們會(huì)將這些依賴也一并打包到發(fā)布包里,隨工具包一起提供。對(duì)于這些間接依賴,在封裝的代碼里,并不需要顯式的load,因?yàn)镻ython與Java的實(shí)現(xiàn)里,加載動(dòng)態(tài)庫,最終調(diào)用的都是系統(tǒng)函數(shù)dlopen。這個(gè)函數(shù)在加載目標(biāo)動(dòng)態(tài)庫時(shí),會(huì)自動(dòng)的加載它的間接依賴。所以我們所需要做的,就只是將這些依賴放置到dlopen能夠查找到路徑下。dlopen查找依賴的順序如下:
- 從dlopen調(diào)用方ELF(Executable and Linkable Format)的DT_RPATH所指定的目錄下尋找,ELF是so的文件格式,這里的DT_RPATH是寫在動(dòng)態(tài)庫文件的,常規(guī)手段下,我們無法修改這個(gè)部分。
- 從環(huán)境變量LD_LIBRARY_PATH所指定的目錄下尋找,這是最常用的指定動(dòng)態(tài)庫路徑的方式。
- 從dlopen調(diào)用方ELF的DT_RUNPATH所指定的目錄下尋找,同樣是在so文件中指定的路徑。
- 從/etc/ld.so.cache尋找,需要修改/etc/ld.so.conf文件構(gòu)建的目標(biāo)緩存,因?yàn)樾枰猺oot權(quán)限,所以在實(shí)際生產(chǎn)中,一般很少修改。
- 從/lib尋找, 系統(tǒng)目錄,一般存放系統(tǒng)依賴的動(dòng)態(tài)庫。
- 從/usr/lib尋找,通過root安裝的動(dòng)態(tài)庫,同樣因?yàn)樾枰猺oot權(quán)限,生產(chǎn)中,很少使用。
從上述查找順序中可以看出,對(duì)于依賴管理的最好方式,是通過指定LD_LIBRARY_PATH變量的方式,使其包含我們的工具包中的動(dòng)態(tài)庫資源所在的路徑。另外,對(duì)于Java程序而言,我們也可以通過指定java.library.path運(yùn)行參數(shù)的方式來指定動(dòng)態(tài)庫的位置。Java程序會(huì)將java.library.path與動(dòng)態(tài)庫文件名拼接到一起作為絕對(duì)路徑傳遞給dlopen,其加載順序排在上述順序之前。
最后,在Java中還有一個(gè)細(xì)節(jié)需要注意,我們發(fā)布的工具包是以JAR包形式提供,JAR包本質(zhì)上是一個(gè)壓縮包,在Java程序中,我們能夠直接通過Native.load()方法,直接加載位于項(xiàng)目resources目錄里的so,這些資源文件打包后,會(huì)被放到JAR包中的根目錄。
但是dlopen無法加載這個(gè)目錄。對(duì)于這一問題,最好的方案可以參考【3.1.3生成動(dòng)態(tài)庫】一節(jié)中的打包方法,將依賴的動(dòng)態(tài)庫合成一個(gè)so,這樣無須做任何環(huán)境配置,開箱即用。但是對(duì)于諸如libstdc++.so.6等無法打包在一個(gè)so的中系統(tǒng)庫,更為通用的做法是,在服務(wù)初始化時(shí)將so文件從JAR包中拷貝至本地某個(gè)目錄,并指定LD_LIBRARY_PATH包含該目錄。
4. 原理介紹
4.1 為什么需要一個(gè)c_wrapper
實(shí)現(xiàn)方案一節(jié)中提到Python/Java不能直接調(diào)用C++接口,要先對(duì)C++中對(duì)外提供的接口用C語言的形式進(jìn)行封裝。這里根本原因在于使用動(dòng)態(tài)庫中的接口前,需要根據(jù)函數(shù)名查找接口在內(nèi)存中的地址,動(dòng)態(tài)庫中函數(shù)的尋址通過系統(tǒng)函數(shù)dlsym實(shí)現(xiàn),dlsym是嚴(yán)格按照傳入的函數(shù)名尋址。
在C語言中,函數(shù)簽名即為代碼函數(shù)的名稱,而在C++語言中,因?yàn)樾枰С趾瘮?shù)重載,可能會(huì)有多個(gè)同名函數(shù)。為了保證簽名唯一,C++通過name mangling機(jī)制為相同名字不同實(shí)現(xiàn)的函數(shù)生成不同的簽名,生成的簽名會(huì)是一個(gè)像__Z4funcPN4printE這樣的字符串,無法被dlsym識(shí)別(注:Linux系統(tǒng)下可執(zhí)行程序或者動(dòng)態(tài)庫多是以ELF格式組織二進(jìn)制數(shù)據(jù),其中所有的非靜態(tài)函數(shù)(non-static)以“符號(hào)(symbol)”作為唯一標(biāo)識(shí),用于在鏈接過程和執(zhí)行過程中區(qū)分不同的函數(shù),并在執(zhí)行時(shí)映射到具體的指令地址,這個(gè)“符號(hào)”我們通常稱之為函數(shù)簽名)。
為了解決這個(gè)問題,我們需要通過extern "C" 指定函數(shù)使用C的簽名方式進(jìn)行編譯。因此當(dāng)依賴的動(dòng)態(tài)庫是C++庫時(shí),需要通過一個(gè)c_wrapper模塊作為橋接。而對(duì)于依賴庫是C語言編譯的動(dòng)態(tài)庫時(shí),則不需要這個(gè)模塊,可以直接調(diào)用。
4.2 跨語言調(diào)用如何實(shí)現(xiàn)參數(shù)傳遞
C/C++函數(shù)調(diào)用的標(biāo)準(zhǔn)過程如下:
- 在內(nèi)存的??臻g中為被調(diào)函數(shù)分配一個(gè)棧幀,用來存放被調(diào)函數(shù)的形參、局部變量和返回地址。
- 將實(shí)參的值復(fù)制給相應(yīng)的形參變量(可以是指針、引用、值拷貝)。
- 控制流轉(zhuǎn)移到被調(diào)函數(shù)的起始位置,并執(zhí)行。
- 控制流返回到函數(shù)調(diào)用點(diǎn),并將返回值給到調(diào)用方,同時(shí)棧幀釋放。
由以上過程可知,函數(shù)調(diào)用涉及內(nèi)存的申請(qǐng)釋放、實(shí)參到形參的拷貝等,Python/Java這種基于虛擬機(jī)運(yùn)行的程序,在其虛擬機(jī)內(nèi)部也同樣遵守上述過程,但涉及到調(diào)用非原生語言實(shí)現(xiàn)的動(dòng)態(tài)庫程序時(shí),調(diào)用過程是怎樣的呢?
由于Python/Java的調(diào)用過程基本一致,我們以Java的調(diào)用過程為例來進(jìn)行解釋,對(duì)于Python的調(diào)用過程不再贅述。
4.2.1 內(nèi)存管理
在Java的世界里,內(nèi)存由JVM統(tǒng)一進(jìn)行管理,JVM的內(nèi)存由棧區(qū)、堆區(qū)、方法區(qū)構(gòu)成,在較為詳細(xì)的資料中,還會(huì)提到native heap與native stack,其實(shí)這個(gè)問題,我們不從JVM的角度去看,而是從操作系統(tǒng)層面出發(fā)來理解會(huì)更為簡(jiǎn)單直觀。以Linux系統(tǒng)下為例,首先JVM名義上是一個(gè)虛擬機(jī),但是其本質(zhì)就是跑在操作系統(tǒng)上的一個(gè)進(jìn)程,因此這個(gè)進(jìn)程的內(nèi)存會(huì)存在如下左圖所示劃分。而JVM的內(nèi)存管理實(shí)質(zhì)上是在進(jìn)程的堆上進(jìn)行重新劃分,自己又“虛擬”出Java世界里的堆棧。如右圖所示,native的棧區(qū)就是JVM進(jìn)程的棧區(qū),進(jìn)程的堆區(qū)一部分用于JVM進(jìn)行管理,剩余的則可以給native方法進(jìn)行分配使用。
圖 3
4.2.2 調(diào)用過程
前文提到,native方法調(diào)用前,需要將其所在的動(dòng)態(tài)庫加載到內(nèi)存中,這個(gè)過程是利用Linux的dlopen實(shí)現(xiàn)的,JVM會(huì)把動(dòng)態(tài)庫中的代碼片段放到Native Code區(qū)域,同時(shí)會(huì)在JVM Bytecode區(qū)域保存一份native方法名與其所在Native Code里的內(nèi)存地址映射。
一次native方法的調(diào)用步驟,大致分為四步:
- 從JVM Bytecode獲取native方法的地址。
- 準(zhǔn)備方法所需的參數(shù)。
- 切換到native棧中,執(zhí)行native方法。
- native方法出棧后,切換回JVM方法,JVM將結(jié)果拷貝至JVM的棧或堆中。
圖 4
由上述步驟可以看出,native方法的調(diào)用同樣涉及參數(shù)的拷貝,并且其拷貝是建立在JVM堆棧和原生堆棧之間。
對(duì)于原生數(shù)據(jù)類型,參數(shù)是通過值拷貝方式與native方法地址一起入棧。而對(duì)于復(fù)雜數(shù)據(jù)類型,則需要一套協(xié)議,將Java中的object映射到C/C++中能識(shí)別的數(shù)據(jù)字節(jié)。原因是JVM與C語言中的內(nèi)存排布差異較大,不能直接內(nèi)存拷貝,這些差異主要包括:
- 類型長度不同,比如char在Java里為16字節(jié),在C里面卻是8個(gè)字節(jié)。
- JVM與操作系統(tǒng)的字節(jié)順序(Big Endian還是Little Endian)可能不一致。
- JVM的對(duì)象中,會(huì)包含一些meta信息,而C里的struct則只是基礎(chǔ)類型的并列排布,同樣Java中沒有指針,也需要進(jìn)行封裝和映射。
圖 5
上圖展示了native方法調(diào)用過程中參數(shù)傳遞的過程,其中映射拷貝在JNI中是由C/C++鏈接部分的膠水代碼實(shí)現(xiàn),類型的映射定義在jni.h中。
Java基本類型與C基本類型的映射(通過值傳遞。將Java對(duì)象在JVM內(nèi)存里的值拷貝至棧幀的形參位置):
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble;
typedef jint jsize;
Java復(fù)雜類型與C復(fù)雜類型的映射(通過指針傳遞。首先根據(jù)基本類型一一映射,將組裝好的新對(duì)象的地址拷貝至棧幀的形參位置):
typedef _jobject *jobject;
typedef _jclass *jclass;
typedef _jthrowable *jthrowable;
typedef _jstring *jstring;
typedef _jarray *jarray;
注:在Java中,非原生類型均是Object的派生類,多個(gè)object的數(shù)組本身也是一個(gè)object,每個(gè)object的類型是一個(gè)class,同時(shí)class本身也是一個(gè)object。
class _jobject {};
class _jclass : public _jobject {};
class _jthrowable : public _jobject {};
class _jarray : public _jobject {};
class _jcharArray : public _jarray {};
class _jobjectArray : public
分享題目:Linux下跨語言調(diào)用C++實(shí)踐
當(dāng)前地址:http://www.5511xx.com/article/ccccgsh.html


咨詢
建站咨詢
