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

1 背景介紹
2 方案概述
3 實現(xiàn)詳情
- 3.1 功能代碼
- 3.2 打包發(fā)布
- 3.3 業(yè)務(wù)使用
- 3.4 易用性優(yōu)化
4. 原理介紹
- 4.1 為什么需要一個c_wrapper
- 4.2 跨語言調(diào)用如何實現(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ǔ)信號,應(yīng)用于搜索的召回、排序、展示等多個環(huán)節(jié),對搜索基礎(chǔ)體驗至關(guān)重要。該服務(wù)的線上主體程序基于C++語言開發(fā),服務(wù)中會加載大量的詞表數(shù)據(jù)、預(yù)估模型等,這些數(shù)據(jù)與模型的離線生產(chǎn)過程有很多文本解析能力需要與線上服務(wù)保持一致,從而保證效果層面的一致性,如文本歸一化、分詞等。
而這些離線生產(chǎn)過程通常用Python與Java實現(xiàn)。如果在線、離線用不同語言各自開發(fā)一份,則很難維持策略與效果上的統(tǒng)一。同時這些能力會有不斷的迭代,在這種動態(tài)場景下,不斷維護(hù)多語言版本的效果打平,給我們的日常迭代帶來了極大的成本。因此,我們嘗試通過跨語言調(diào)用動態(tài)鏈接庫的技術(shù)解決這個問題,即開發(fā)一次基于C++的so,通過不同語言的鏈接層封裝成不同語言的組件庫,并投入到對應(yīng)的生產(chǎn)過程。這種方案的優(yōu)勢非常明顯,主體的業(yè)務(wù)邏輯只需要開發(fā)一次,封裝層只需要極少量的代碼,主體業(yè)務(wù)迭代升級,其它語言幾乎不需要改動,只需要包含最新的動態(tài)鏈接庫,發(fā)布最新版本即可。同時C++作為更底層的語言,在很多場景下,它的計算效率更高,硬件資源利用率更高,也為我們帶來了一些性能上的優(yōu)勢。
本文對我們在實際生產(chǎn)中嘗試這一技術(shù)方案時,遇到的問題與一些實踐經(jīng)驗做了完整的梳理,希望能為大家提供一些參考或幫助。
2 方案概述
為了達(dá)到業(yè)務(wù)方開箱即用的目的,綜合考慮C++、Python、Java用戶的使用習(xí)慣,我們設(shè)計了如下的協(xié)作結(jié)構(gòu):
圖 1
3 實現(xiàn)詳情
Python、Java支持調(diào)用C接口,但不支持調(diào)用C++接口,因此對于C++語言實現(xiàn)的接口,必須轉(zhuǎn)換為C語言實現(xiàn)。為了不修改原始C++代碼,在C++接口上層用C語言進(jìn)行一次封裝,這部分代碼通常被稱為“膠水代碼”(Glue Code)。具體方案如下圖所示:
圖 2
本章節(jié)各部分內(nèi)容如下:
- 【功能代碼】部分,通過打印字符串的例子來講述各語言部分的編碼工作。
- 【打包發(fā)布】部分,介紹如何將生成的動態(tài)庫作為資源文件與Python、Java代碼打包在一起發(fā)布到倉庫,以降低使用方的接入成本。
- 【業(yè)務(wù)使用】部分,介紹開箱即用的使用示例。
- 【易用性優(yōu)化】部分,結(jié)合實際使用中遇到的問題,講述了對于Python版本兼容,以及動態(tài)庫依賴問題的處理方式。
3.1 功能代碼
3.1.1 C++代碼
作為示例,實現(xiàn)一個打印字符串的功能。為了模擬實際的工業(yè)場景,對以下代碼進(jìn)行編譯,分別生成動態(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代碼
如上文所述,需要對C++庫進(jìn)行封裝,改造成對外提供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 生成動態(tài)庫
為了支持Python與Java的跨語言調(diào)用,我們需要對封裝好的接口生成動態(tài)庫,生成動態(tài)庫的方式有以下三種。
方式一:源碼依賴方式,將c_wrapper和C++代碼一起編譯生成libstr_print.so。這種方式業(yè)務(wù)方只需要依賴一個so,使用成本較小,但是需要獲取到C++源碼。對于一些現(xiàn)成的動態(tài)庫,可能不適用。
g++ -o libstr_print.so str_print.cpp c_wrapper.cpp -fPIC -shared
方式二:動態(tài)鏈接方式,這種方式生成的libstr_print.so,發(fā)布時需要攜帶上其依賴庫libstr_print_cpp.so。業(yè)務(wù)方需要同時依賴兩個so,使用的成本相對要高,但是不必提供原動態(tài)庫的源碼。
g++ -o libstr_print.so c_wrapper.cpp -fPIC -shared -L. -lstr_print_cpp
方式三:靜態(tài)鏈接方式,這種方式生成的libstr_print.so,發(fā)布時無需攜帶上libstr_print_cpp.so。業(yè)務(wù)方只需依賴一個so,不必依賴源碼,但是需要提供靜態(tài)庫。
g++ c_wrapper.cpp libstr_print_cpp.a -fPIC -shared -o libstr_print.so
上述三種方式,各自有適用場景和優(yōu)缺點。在我們本次的業(yè)務(wù)場景下,因為工具庫與封裝庫均由我們自己開發(fā),能夠獲取到源碼,因此選擇第一種方式,業(yè)務(wù)方依賴更加簡單。
3.1.4 Python接入代碼
Python標(biāo)準(zhǔn)庫自帶的ctypes可以實現(xiàn)加載C的動態(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會返回一個指向動態(tài)庫的實例,通過它可以在Python里直接調(diào)用該庫中的函數(shù)。argtypes與restype是動態(tài)庫中函數(shù)的參數(shù)屬性,前者是一個ctypes類型的列表或元組,用于指定動態(tài)庫中函數(shù)接口的參數(shù)類型,后者是函數(shù)的返回類型(默認(rèn)是c_int,可以不指定,對于非c_int型需要顯示指定)。該部分涉及到的參數(shù)類型映射,以及如何向函數(shù)中傳遞struct、指針等高級類型,可以參考附錄中的文檔。
3.1.5 Java接入代碼
Java調(diào)用C lib有JNI與JNA兩種方式,從使用便捷性來看,更推薦JNA方式。
3.1.5.1 JNI接入
Java從1.1版本開始支持JNI接口協(xié)議,用于實現(xiàn)Java語言調(diào)用C/C++動態(tài)庫。JNI方式下,前文提到的c_wrapper模塊不再適用,JNI協(xié)議本身提供了適配層的接口定義,需要按照這個定義進(jìn)行實現(xiàn)。JNI方式的具體接入步驟為:
Java代碼里,在需要跨語言調(diào)用的方法上,增加native關(guān)鍵字,用以聲明這是一個本地方法。
import java.lang.String;
public class JniDemo {
public native void print(String text);
}
通過javah命令,將代碼中的native方法生成對應(yīng)的C語言的頭文件。這個頭文件類似于前文提到的c_wrapper作用。
javah JniDemo
得到的頭文件如下(為節(jié)省篇幅,這里簡化了一些注釋和宏):
#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)實現(xiàn)。
JNIEXPORT和JNICALL是JNI中定義的兩個宏,JNIEXPORT標(biāo)識了支持在外部程序代碼中調(diào)用該動態(tài)庫中的方法,JNICALL定義了函數(shù)調(diào)用時參數(shù)的入棧出棧約定。
Java_JniDemo_print是一個自動生成的函數(shù)名,它的格式是固定,由Java_{className}_{methodName}構(gòu)成,JNI會按照這個約定去注冊Java方法與C函數(shù)的映射。
三個參數(shù)里,前兩個是固定的。JNIEnv中封裝了jni.h里的一些工具方法,jobject指向Java中的調(diào)用類,即JniDemo,通過它可以找到Java里class中的成員變量在C的堆棧中的拷貝。jstring指向傳入?yún)?shù)text,這是對于Java中String類型的一個映射。有關(guān)類型映射的具體內(nèi)容,會在后文詳細(xì)展開。
編寫實現(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);
}
編譯生成動態(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++的橋接,實現(xiàn)了跨語言調(diào)用協(xié)議。這一功能在Android系統(tǒng)中一些圖形計算相關(guān)的Java程序下有著大量應(yīng)用。一方面能夠通過Java調(diào)用大量操作系統(tǒng)底層庫,極大的減少了JDK上的驅(qū)動開發(fā)的工作量,另一方面能夠更充分的利用硬件性能。但是通過3.1.5.1中的描述也可以看到,JNI的實現(xiàn)方式本身的實現(xiàn)成本還是比較高的。尤其橋接層的C/C++代碼的編寫,在處理復(fù)雜類型的參數(shù)傳遞時,開發(fā)成本較大。為了優(yōu)化這個過程,Sun公司主導(dǎo)了JNA(Java Native Access)開源工程的工作。
3.1.5.2 JNA接入
JNA是在JNI基礎(chǔ)上實現(xiàn)的編程框架,它提供了C語言動態(tài)轉(zhuǎn)發(fā)器,實現(xiàn)了Java類型到C類型的自動轉(zhuǎn)換。因此,Java開發(fā)人員只要在一個Java接口中描述目標(biāo)native library的函數(shù)與結(jié)構(gòu),不再需要編寫任何Native/JNI代碼,極大的降低了Java調(diào)用本地共享庫的開發(fā)難度。
JNA的使用方法如下:
在Java項目中引入JNA庫。
com.sun.jna
jna
5.4.0
聲明與動態(tài)庫對應(yīng)的Java接口類。
public interface CLibrary extends Library {
void str_print(String text); // 方法名和動態(tài)庫接口一致,參數(shù)類型需要用Java里的類型表示,執(zhí)行時會做類型映射,原理介紹章節(jié)會有詳細(xì)解釋
}
加載動態(tài)鏈接庫,并實現(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);
}
}
對比可以發(fā)現(xiàn),相比于JNI,JNA不再需要指定native關(guān)鍵字,不再需要生成JNI部分C代碼,也不再需要顯示的做參數(shù)類型轉(zhuǎn)化,極大地提高了調(diào)用動態(tài)庫的效率。
3.2 打包發(fā)布
為了做到開箱即用,我們將動態(tài)庫與對應(yīng)語言代碼打包在一起,并自動準(zhǔn)備好對應(yīng)依賴環(huán)境。這樣使用方只需要安裝對應(yīng)的庫,并引入到工程中,就可以直接開始調(diào)用。這里需要解釋的是,我們沒有將so發(fā)布到運(yùn)行機(jī)器上,而是將其和接口代碼一并發(fā)布至代碼倉庫,原因是我們所開發(fā)的工具代碼可能被不同業(yè)務(wù)、不同背景(非C++)團(tuán)隊使用,不能保證各個業(yè)務(wù)方團(tuán)隊都使用統(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 動態(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接口
對于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目錄,并將依賴的動態(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ā)用戶一直詬病的槽點。因為工具面向不同的業(yè)務(wù)團(tuán)隊,我們沒有辦法強(qiáng)制要求使用統(tǒng)一的Python版本,但是我們可以通過對工具庫做一下簡單處理,實現(xiàn)兩個版本的兼容。Python版本兼容里,需要注意兩方面的問題:
- 語法兼容
- 數(shù)據(jù)編碼
Python代碼的封裝里,基本不牽扯語法兼容問題,我們的工作主要集中在數(shù)據(jù)編碼問題上。由于Python 3的str類型使用的是unicode編碼,而在C中,我們需要的char* 是utf8編碼,因此需要對于傳入的字符串做utf8編碼處理,對于C語言返回的字符串,做utf8轉(zhuǎn)換成unicode的解碼處理。于是對于上例子,我們做了如下改造:
# -*- 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)用的動態(tài)庫,會依賴其它動態(tài)庫,比如當(dāng)我們依賴的gcc/g++版本與運(yùn)行環(huán)境上的不一致時,時常會遇到glibc_X.XX not found的問題,這時需要我們提供指定版本的libstdc.so與libstdc++.so.6。
為了實現(xiàn)開箱即用的目標(biāo),在依賴并不復(fù)雜的情況下,我們會將這些依賴也一并打包到發(fā)布包里,隨工具包一起提供。對于這些間接依賴,在封裝的代碼里,并不需要顯式的load,因為Python與Java的實現(xiàn)里,加載動態(tài)庫,最終調(diào)用的都是系統(tǒng)函數(shù)dlopen。這個函數(shù)在加載目標(biāo)動態(tài)庫時,會自動的加載它的間接依賴。所以我們所需要做的,就只是將這些依賴放置到dlopen能夠查找到路徑下。dlopen查找依賴的順序如下:
- 從dlopen調(diào)用方ELF(Executable and Linkable Format)的DT_RPATH所指定的目錄下尋找,ELF是so的文件格式,這里的DT_RPATH是寫在動態(tài)庫文件的,常規(guī)手段下,我們無法修改這個部分。
- 從環(huán)境變量LD_LIBRARY_PATH所指定的目錄下尋找,這是最常用的指定動態(tài)庫路徑的方式。
- 從dlopen調(diào)用方ELF的DT_RUNPATH所指定的目錄下尋找,同樣是在so文件中指定的路徑。
- 從/etc/ld.so.cache尋找,需要修改/etc/ld.so.conf文件構(gòu)建的目標(biāo)緩存,因為需要root權(quán)限,所以在實際生產(chǎn)中,一般很少修改。
- 從/lib尋找, 系統(tǒng)目錄,一般存放系統(tǒng)依賴的動態(tài)庫。
- 從/usr/lib尋找,通過root安裝的動態(tài)庫,同樣因為需要root權(quán)限,生產(chǎn)中,很少使用。
從上述查找順序中可以看出,對于依賴管理的最好方式,是通過指定LD_LIBRARY_PATH變量的方式,使其包含我們的工具包中的動態(tài)庫資源所在的路徑。另外,對于Java程序而言,我們也可以通過指定java.library.path運(yùn)行參數(shù)的方式來指定動態(tài)庫的位置。Java程序會將java.library.path與動態(tài)庫文件名拼接到一起作為絕對路徑傳遞給dlopen,其加載順序排在上述順序之前。
最后,在Java中還有一個細(xì)節(jié)需要注意,我們發(fā)布的工具包是以JAR包形式提供,JAR包本質(zhì)上是一個壓縮包,在Java程序中,我們能夠直接通過Native.load()方法,直接加載位于項目resources目錄里的so,這些資源文件打包后,會被放到JAR包中的根目錄。
但是dlopen無法加載這個目錄。對于這一問題,最好的方案可以參考【3.1.3生成動態(tài)庫】一節(jié)中的打包方法,將依賴的動態(tài)庫合成一個so,這樣無須做任何環(huán)境配置,開箱即用。但是對于諸如libstdc++.so.6等無法打包在一個so的中系統(tǒng)庫,更為通用的做法是,在服務(wù)初始化時將so文件從JAR包中拷貝至本地某個目錄,并指定LD_LIBRARY_PATH包含該目錄。
4. 原理介紹
4.1 為什么需要一個c_wrapper
實現(xiàn)方案一節(jié)中提到Python/Java不能直接調(diào)用C++接口,要先對C++中對外提供的接口用C語言的形式進(jìn)行封裝。這里根本原因在于使用動態(tài)庫中的接口前,需要根據(jù)函數(shù)名查找接口在內(nèi)存中的地址,動態(tài)庫中函數(shù)的尋址通過系統(tǒng)函數(shù)dlsym實現(xiàn),dlsym是嚴(yán)格按照傳入的函數(shù)名尋址。
在C語言中,函數(shù)簽名即為代碼函數(shù)的名稱,而在C++語言中,因為需要支持函數(shù)重載,可能會有多個同名函數(shù)。為了保證簽名唯一,C++通過name mangling機(jī)制為相同名字不同實現(xiàn)的函數(shù)生成不同的簽名,生成的簽名會是一個像__Z4funcPN4printE這樣的字符串,無法被dlsym識別(注:Linux系統(tǒng)下可執(zhí)行程序或者動態(tài)庫多是以ELF格式組織二進(jìn)制數(shù)據(jù),其中所有的非靜態(tài)函數(shù)(non-static)以“符號(symbol)”作為唯一標(biāo)識,用于在鏈接過程和執(zhí)行過程中區(qū)分不同的函數(shù),并在執(zhí)行時映射到具體的指令地址,這個“符號”我們通常稱之為函數(shù)簽名)。
為了解決這個問題,我們需要通過extern "C" 指定函數(shù)使用C的簽名方式進(jìn)行編譯。因此當(dāng)依賴的動態(tài)庫是C++庫時,需要通過一個c_wrapper模塊作為橋接。而對于依賴庫是C語言編譯的動態(tài)庫時,則不需要這個模塊,可以直接調(diào)用。
4.2 跨語言調(diào)用如何實現(xiàn)參數(shù)傳遞
C/C++函數(shù)調(diào)用的標(biāo)準(zhǔn)過程如下:
- 在內(nèi)存的??臻g中為被調(diào)函數(shù)分配一個棧幀,用來存放被調(diào)函數(shù)的形參、局部變量和返回地址。
- 將實參的值復(fù)制給相應(yīng)的形參變量(可以是指針、引用、值拷貝)。
- 控制流轉(zhuǎn)移到被調(diào)函數(shù)的起始位置,并執(zhí)行。
- 控制流返回到函數(shù)調(diào)用點,并將返回值給到調(diào)用方,同時棧幀釋放。
由以上過程可知,函數(shù)調(diào)用涉及內(nèi)存的申請釋放、實參到形參的拷貝等,Python/Java這種基于虛擬機(jī)運(yùn)行的程序,在其虛擬機(jī)內(nèi)部也同樣遵守上述過程,但涉及到調(diào)用非原生語言實現(xiàn)的動態(tài)庫程序時,調(diào)用過程是怎樣的呢?
由于Python/Java的調(diào)用過程基本一致,我們以Java的調(diào)用過程為例來進(jìn)行解釋,對于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ì)的資料中,還會提到native heap與native stack,其實這個問題,我們不從JVM的角度去看,而是從操作系統(tǒng)層面出發(fā)來理解會更為簡單直觀。以Linux系統(tǒng)下為例,首先JVM名義上是一個虛擬機(jī),但是其本質(zhì)就是跑在操作系統(tǒng)上的一個進(jìn)程,因此這個進(jìn)程的內(nèi)存會存在如下左圖所示劃分。而JVM的內(nèi)存管理實質(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)用前,需要將其所在的動態(tài)庫加載到內(nèi)存中,這個過程是利用Linux的dlopen實現(xiàn)的,JVM會把動態(tài)庫中的代碼片段放到Native Code區(qū)域,同時會在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堆棧和原生堆棧之間。
對于原生數(shù)據(jù)類型,參數(shù)是通過值拷貝方式與native方法地址一起入棧。而對于復(fù)雜數(shù)據(jù)類型,則需要一套協(xié)議,將Java中的object映射到C/C++中能識別的數(shù)據(jù)字節(jié)。原因是JVM與C語言中的內(nèi)存排布差異較大,不能直接內(nèi)存拷貝,這些差異主要包括:
- 類型長度不同,比如char在Java里為16字節(jié),在C里面卻是8個字節(jié)。
- JVM與操作系統(tǒng)的字節(jié)順序(Big Endian還是Little Endian)可能不一致。
- JVM的對象中,會包含一些meta信息,而C里的struct則只是基礎(chǔ)類型的并列排布,同樣Java中沒有指針,也需要進(jìn)行封裝和映射。
圖 5
上圖展示了native方法調(diào)用過程中參數(shù)傳遞的過程,其中映射拷貝在JNI中是由C/C++鏈接部分的膠水代碼實現(xiàn),類型的映射定義在jni.h中。
Java基本類型與C基本類型的映射(通過值傳遞。將Java對象在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ù)基本類型一一映射,將組裝好的新對象的地址拷貝至棧幀的形參位置):
typedef _jobject *jobject;
typedef _jclass *jclass;
typedef _jthrowable *jthrowable;
typedef _jstring *jstring;
typedef _jarray *jarray;
注:在Java中,非原生類型均是Object的派生類,多個object的數(shù)組本身也是一個object,每個object的類型是一個class,同時class本身也是一個object。
class _jobject {};
class _jclass : public _jobject {};
class _jthrowable : public _jobject {};
class _jarray : public _jobject {};
class _jcharArray : public _jarray {};
class _jobjectArray : public
網(wǎng)站欄目:Linux下跨語言調(diào)用C++實踐
當(dāng)前URL:http://fisionsoft.com.cn/article/ccccgsh.html


咨詢
建站咨詢
