av激情亚洲男人的天堂国语,日韩欧美精品一中文字幕,无码av一区二区三区无码,国产又色又爽又刺激的a片,国产又色又爽又刺激的a片

Linux下跨語言調(diào)用C++實踐

不同的開發(fā)語言適合不同的領(lǐng)域,例如Python適合做數(shù)據(jù)分析,C++適合做系統(tǒng)的底層開發(fā),假如它們需要用到相同功能的基礎組件,組件使用多種語言分別開發(fā)的話,不僅增加了開發(fā)和維護成本,而且不能確保多種語言間在處理效果上是一致的。本文以美團搜索實際場景下的案例,講述在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è)務使用
  • 3.4 易用性優(yōu)化

4. 原理介紹

  • 4.1 為什么需要一個c_wrapper
  • 4.2 跨語言調(diào)用如何實現(xiàn)參數(shù)傳遞
  • 4.3 擴展閱讀(JNA直接映射)
  • 4.4 性能分析

5 應用案例

  • 5.1 離線任務中的應用
  • 5.2 在線服務中的應用

6 總結(jié)

1 背景

查詢理解(QU, Query Understanding)是美團搜索的核心模塊,主要職責是理解用戶查詢,生成查詢意圖、成分、改寫等基礎信號,應用于搜索的召回、排序、展示等多個環(huán)節(jié),對搜索基礎體驗至關(guān)重要。該服務的線上主體程序基于C++語言開發(fā),服務中會加載大量的詞表數(shù)據(jù)、預估模型等,這些數(shù)據(jù)與模型的離線生產(chǎn)過程有很多文本解析能力需要與線上服務保持一致,從而保證效果層面的一致性,如文本歸一化、分詞等。

而這些離線生產(chǎn)過程通常用Python與Java實現(xiàn)。如果在線、離線用不同語言各自開發(fā)一份,則很難維持策略與效果上的統(tǒng)一。同時這些能力會有不斷的迭代,在這種動態(tài)場景下,不斷維護多語言版本的效果打平,給我們的日常迭代帶來了極大的成本。因此,我們嘗試通過跨語言調(diào)用動態(tài)鏈接庫的技術(shù)解決這個問題,即開發(fā)一次基于C++的so,通過不同語言的鏈接層封裝成不同語言的組件庫,并投入到對應的生產(chǎn)過程。這種方案的優(yōu)勢非常明顯,主體的業(yè)務邏輯只需要開發(fā)一次,封裝層只需要極少量的代碼,主體業(yè)務迭代升級,其它語言幾乎不需要改動,只需要包含最新的動態(tài)鏈接庫,發(fā)布最新版本即可。同時C++作為更底層的語言,在很多場景下,它的計算效率更高,硬件資源利用率更高,也為我們帶來了一些性能上的優(yōu)勢。

本文對我們在實際生產(chǎn)中嘗試這一技術(shù)方案時,遇到的問題與一些實踐經(jīng)驗做了完整的梳理,希望能為大家提供一些參考或幫助。

2 方案概述

為了達到業(yè)務方開箱即用的目的,綜合考慮C++、Python、Java用戶的使用習慣,我們設計了如下的協(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語言進行一次封裝,這部分代碼通常被稱為“膠水代碼”(Glue Code)。具體方案如下圖所示:

圖 2

本章節(jié)各部分內(nèi)容如下:

  • 【功能代碼】部分,通過打印字符串的例子來講述各語言部分的編碼工作。
  • 【打包發(fā)布】部分,介紹如何將生成的動態(tài)庫作為資源文件與Python、Java代碼打包在一起發(fā)布到倉庫,以降低使用方的接入成本。
  • 【業(yè)務使用】部分,介紹開箱即用的使用示例。
  • 【易用性優(yōu)化】部分,結(jié)合實際使用中遇到的問題,講述了對于Python版本兼容,以及動態(tài)庫依賴問題的處理方式。

3.1 功能代碼

3.1.1 C++代碼

作為示例,實現(xiàn)一個打印字符串的功能。為了模擬實際的工業(yè)場景,對以下代碼進行編譯,分別生成動態(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++庫進行封裝,改造成對外提供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è)務方只需要依賴一個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è)務方需要同時依賴兩個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è)務方只需依賴一個so,不必依賴源碼,但是需要提供靜態(tài)庫。

g++ c_wrapper.cpp libstr_print_cpp.a -fPIC -shared -o libstr_print.so

上述三種方式,各自有適用場景和優(yōu)缺點。在我們本次的業(yè)務場景下,因為工具庫與封裝庫均由我們自己開發(fā),能夠獲取到源碼,因此選擇第一種方式,業(yè)務方依賴更加簡單。

3.1.4 Python接入代碼

Python標準庫自帶的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ù)的返回類型(默認是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é)議本身提供了適配層的接口定義,需要按照這個定義進行實現(xiàn)。JNI方式的具體接入步驟為:

Java代碼里,在需要跨語言調(diào)用的方法上,增加native關(guān)鍵字,用以聲明這是一個本地方法。

import java.lang.String;
public class JniDemo {
public native void print(String text);
}

通過javah命令,將代碼中的native方法生成對應的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標識了支持在外部程序代碼中調(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)容,會在后文詳細展開。

編寫實現(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

編譯運行。

java -Djava.library.path= JniDemo

JNI機制通過一層C/C++的橋接,實現(xiàn)了跨語言調(diào)用協(xié)議。這一功能在Android系統(tǒng)中一些圖形計算相關(guān)的Java程序下有著大量應用。一方面能夠通過Java調(diào)用大量操作系統(tǒng)底層庫,極大的減少了JDK上的驅(qū)動開發(fā)的工作量,另一方面能夠更充分的利用硬件性能。但是通過3.1.5.1中的描述也可以看到,JNI的實現(xiàn)方式本身的實現(xiàn)成本還是比較高的。尤其橋接層的C/C++代碼的編寫,在處理復雜類型的參數(shù)傳遞時,開發(fā)成本較大。為了優(yōu)化這個過程,Sun公司主導了JNA(Java Native Access)開源工程的工作。

3.1.5.2 JNA接入

JNA是在JNI基礎上實現(xiàn)的編程框架,它提供了C語言動態(tài)轉(zhuǎn)發(fā)器,實現(xiàn)了Java類型到C類型的自動轉(zhuǎn)換。因此,Java開發(fā)人員只要在一個Java接口中描述目標native library的函數(shù)與結(jié)構(gòu),不再需要編寫任何Native/JNI代碼,極大的降低了Java調(diào)用本地共享庫的開發(fā)難度。

JNA的使用方法如下:

在Java項目中引入JNA庫。


com.sun.jna
jna
5.4.0


聲明與動態(tài)庫對應的Java接口類。

public interface CLibrary extends Library {
void str_print(String text); // 方法名和動態(tài)庫接口一致,參數(shù)類型需要用Java里的類型表示,執(zhí)行時會做類型映射,原理介紹章節(jié)會有詳細解釋
}

加載動態(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)庫與對應語言代碼打包在一起,并自動準備好對應依賴環(huán)境。這樣使用方只需要安裝對應的庫,并引入到工程中,就可以直接開始調(diào)用。這里需要解釋的是,我們沒有將so發(fā)布到運行機器上,而是將其和接口代碼一并發(fā)布至代碼倉庫,原因是我們所開發(fā)的工具代碼可能被不同業(yè)務、不同背景(非C++)團隊使用,不能保證各個業(yè)務方團隊都使用統(tǒng)一的、標準化的運行環(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è)務使用

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è)務團隊,我們沒有辦法強制要求使用統(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)庫,比如當我們依賴的gcc/g++版本與運行環(huán)境上的不一致時,時常會遇到glibc_X.XX not found的問題,這時需要我們提供指定版本的libstdc.so與libstdc++.so.6。

為了實現(xiàn)開箱即用的目標,在依賴并不復雜的情況下,我們會將這些依賴也一并打包到發(fā)布包里,隨工具包一起提供。對于這些間接依賴,在封裝的代碼里,并不需要顯式的load,因為Python與Java的實現(xiàn)里,加載動態(tài)庫,最終調(diào)用的都是系統(tǒng)函數(shù)dlopen。這個函數(shù)在加載目標動態(tài)庫時,會自動的加載它的間接依賴。所以我們所需要做的,就只是將這些依賴放置到dlopen能夠查找到路徑下。dlopen查找依賴的順序如下:

  1. 從dlopen調(diào)用方ELF(Executable and Linkable Format)的DT_RPATH所指定的目錄下尋找,ELF是so的文件格式,這里的DT_RPATH是寫在動態(tài)庫文件的,常規(guī)手段下,我們無法修改這個部分。
  2. 從環(huán)境變量LD_LIBRARY_PATH所指定的目錄下尋找,這是最常用的指定動態(tài)庫路徑的方式。
  3. 從dlopen調(diào)用方ELF的DT_RUNPATH所指定的目錄下尋找,同樣是在so文件中指定的路徑。
  4. 從/etc/ld.so.cache尋找,需要修改/etc/ld.so.conf文件構(gòu)建的目標緩存,因為需要root權(quán)限,所以在實際生產(chǎn)中,一般很少修改。
  5. 從/lib尋找, 系統(tǒng)目錄,一般存放系統(tǒng)依賴的動態(tài)庫。
  6. 從/usr/lib尋找,通過root安裝的動態(tài)庫,同樣因為需要root權(quán)限,生產(chǎn)中,很少使用。

從上述查找順序中可以看出,對于依賴管理的最好方式,是通過指定LD_LIBRARY_PATH變量的方式,使其包含我們的工具包中的動態(tài)庫資源所在的路徑。另外,對于Java程序而言,我們也可以通過指定java.library.path運行參數(shù)的方式來指定動態(tài)庫的位置。Java程序會將java.library.path與動態(tài)庫文件名拼接到一起作為絕對路徑傳遞給dlopen,其加載順序排在上述順序之前。

最后,在Java中還有一個細節(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)庫,更為通用的做法是,在服務初始化時將so文件從JAR包中拷貝至本地某個目錄,并指定LD_LIBRARY_PATH包含該目錄。

4. 原理介紹

4.1 為什么需要一個c_wrapper

實現(xiàn)方案一節(jié)中提到Python/Java不能直接調(diào)用C++接口,要先對C++中對外提供的接口用C語言的形式進行封裝。這里根本原因在于使用動態(tài)庫中的接口前,需要根據(jù)函數(shù)名查找接口在內(nèi)存中的地址,動態(tài)庫中函數(shù)的尋址通過系統(tǒng)函數(shù)dlsym實現(xiàn),dlsym是嚴格按照傳入的函數(shù)名尋址。

在C語言中,函數(shù)簽名即為代碼函數(shù)的名稱,而在C++語言中,因為需要支持函數(shù)重載,可能會有多個同名函數(shù)。為了保證簽名唯一,C++通過name mangling機制為相同名字不同實現(xiàn)的函數(shù)生成不同的簽名,生成的簽名會是一個像__Z4funcPN4printE這樣的字符串,無法被dlsym識別(注:Linux系統(tǒng)下可執(zhí)行程序或者動態(tài)庫多是以ELF格式組織二進制數(shù)據(jù),其中所有的非靜態(tài)函數(shù)(non-static)以“符號(symbol)”作為唯一標識,用于在鏈接過程和執(zhí)行過程中區(qū)分不同的函數(shù),并在執(zhí)行時映射到具體的指令地址,這個“符號”我們通常稱之為函數(shù)簽名)。

為了解決這個問題,我們需要通過extern "C" 指定函數(shù)使用C的簽名方式進行編譯。因此當依賴的動態(tài)庫是C++庫時,需要通過一個c_wrapper模塊作為橋接。而對于依賴庫是C語言編譯的動態(tài)庫時,則不需要這個模塊,可以直接調(diào)用。

4.2 跨語言調(diào)用如何實現(xiàn)參數(shù)傳遞

C/C++函數(shù)調(diào)用的標準過程如下:

  • 在內(nèi)存的??臻g中為被調(diào)函數(shù)分配一個棧幀,用來存放被調(diào)函數(shù)的形參、局部變量和返回地址。
  • 將實參的值復制給相應的形參變量(可以是指針、引用、值拷貝)。
  • 控制流轉(zhuǎn)移到被調(diào)函數(shù)的起始位置,并執(zhí)行。
  • 控制流返回到函數(shù)調(diào)用點,并將返回值給到調(diào)用方,同時棧幀釋放。

由以上過程可知,函數(shù)調(diào)用涉及內(nèi)存的申請釋放、實參到形參的拷貝等,Python/Java這種基于虛擬機運行的程序,在其虛擬機內(nèi)部也同樣遵守上述過程,但涉及到調(diào)用非原生語言實現(xiàn)的動態(tài)庫程序時,調(diào)用過程是怎樣的呢?

由于Python/Java的調(diào)用過程基本一致,我們以Java的調(diào)用過程為例來進行解釋,對于Python的調(diào)用過程不再贅述。

4.2.1 內(nèi)存管理

在Java的世界里,內(nèi)存由JVM統(tǒng)一進行管理,JVM的內(nèi)存由棧區(qū)、堆區(qū)、方法區(qū)構(gòu)成,在較為詳細的資料中,還會提到native heap與native stack,其實這個問題,我們不從JVM的角度去看,而是從操作系統(tǒng)層面出發(fā)來理解會更為簡單直觀。以Linux系統(tǒng)下為例,首先JVM名義上是一個虛擬機,但是其本質(zhì)就是跑在操作系統(tǒng)上的一個進程,因此這個進程的內(nèi)存會存在如下左圖所示劃分。而JVM的內(nèi)存管理實質(zhì)上是在進程的堆上進行重新劃分,自己又“虛擬”出Java世界里的堆棧。如右圖所示,native的棧區(qū)就是JVM進程的棧區(qū),進程的堆區(qū)一部分用于JVM進行管理,剩余的則可以給native方法進行分配使用。

圖 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方法的地址。
  • 準備方法所需的參數(shù)。
  • 切換到native棧中,執(zhí)行native方法。
  • native方法出棧后,切換回JVM方法,JVM將結(jié)果拷貝至JVM的?;蚨阎小?/li>

圖 4

由上述步驟可以看出,native方法的調(diào)用同樣涉及參數(shù)的拷貝,并且其拷貝是建立在JVM堆棧和原生堆棧之間。

對于原生數(shù)據(jù)類型,參數(shù)是通過值拷貝方式與native方法地址一起入棧。而對于復雜數(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則只是基礎類型的并列排布,同樣Java中沒有指針,也需要進行封裝和映射。

圖 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復雜類型與C復雜類型的映射(通過指針傳遞。首先根據(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 當前題目:Linux下跨語言調(diào)用C++實踐
當前網(wǎng)址:http://uogjgqi.cn/article/ccccgsh.html
掃二維碼與項目經(jīng)理溝通

我們在微信上24小時期待你的聲音

解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流