日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
Linux下跨語言調(diào)用C++實(shí)踐

不同的開發(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查找依賴的順序如下:

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