一、NDK簡介
??NDK(Native Development Kit)是鴻蒙提供的Native API、編譯腳本和編譯工具鏈的集合,方便開發(fā)者使用C/C++實現(xiàn)應用的關(guān)鍵功能。一般情況下應用開發(fā)使用ArkTS,ArkTS已經(jīng)能滿足大部分的功能開發(fā),但有些功能還是需要用C/C++來實現(xiàn),像音視頻、直播、美顏、地圖、AI等功能就需要使用C/C++來實現(xiàn)。學習NDK的前提是熟悉C/C++,重點掌握指針。
二、Node-API
??Node-API為開發(fā)者提供了ArkTS/JS與C/C++模塊之間的交互能力。說白了,ArkTS/JS和C/C++要想相互調(diào)用,就需要使用Node-API。
2、1 Node-API的數(shù)據(jù)類型
napi_status
??napi_status是一個枚舉數(shù)據(jù)類型,表示Node-API接口返回的狀態(tài)信息。每當調(diào)用一個Node-API函數(shù),都會返回該值,表示操作成功與否的相關(guān)信息。
typedef enum {
napi_ok,
napi_invalid_arg,
napi_object_expected,
napi_string_expected,
napi_name_expected,
napi_function_expected,
napi_number_expected,
napi_boolean_expected,
napi_array_expected,
napi_generic_failure,
napi_pending_exception,
napi_cancelled,
napi_escape_called_twice,
napi_handle_scope_mismatch,
napi_callback_scope_mismatch,
napi_queue_full,
napi_closing,
napi_bigint_expected,
napi_date_expected,
napi_arraybuffer_expected,
napi_detachable_arraybuffer_expected,
napi_would_deadlock, /* unused */
napi_no_external_buffers_allowed,
napi_cannot_run_js
} napi_status;
napi_value
??napi_value是C的一個結(jié)構(gòu)體指針,表示一個JavaScript對象的引用。ArkTS向C/C++傳遞的參數(shù),C/C++不能直接使用,需要使用napi_value解析。C/C++向ArkTS傳參,直接傳遞napi_value即可。
napi_env
- 用于表示Node-API執(zhí)行時的上下文,Native側(cè)函數(shù)入?yún)?,并傳遞給函數(shù)中的Node-API接口。
- napi_env與JS線程綁定,JS線程退出后,napi_env將失效。
- 禁止緩存napi_env,禁止在不同線程中傳遞napi_env。
三、創(chuàng)建NDK項目
??使用DevEco-Studio可以創(chuàng)建NDK項目,如下圖所示,創(chuàng)建項目的時候選擇Native C++。
??與ArkTS項目相比,NDK項目多了一個cpp目錄。
3、1 CMakeLists.txt
??cpp目錄里面有個CMakeLists.txt,這是cmake的構(gòu)建文件,C/C++就是使用cmake構(gòu)建的。CMakeLists.txt腳本中添加了編譯所需的源代碼、頭文件以及三方庫,開發(fā)者可根據(jù)實際工程添加自定義編譯參數(shù)、函數(shù)聲明、簡單的邏輯控制等。
# the minimum version of CMake.
cmake_minimum_required(VERSION 3.4.1)
project(MyApplication)
# 定義一個變量,并賦值為當前模塊cpp目錄
# ${CMAKE_CURRENT_SOURCE_DIR}指的就是CMakeLists.txt所在路徑
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
# 添加頭文件.h目錄,包括cpp,cpp/include,告訴cmake去這里找到代碼引入的頭文件
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include)
# 聲明一個產(chǎn)物libentry.so,SHARED表示產(chǎn)物為動態(tài)庫,napi_init.cpp為產(chǎn)物的源代碼
add_library(entry SHARED napi_init.cpp)
# 聲明產(chǎn)物entry鏈接時需要的三方庫libace_napi.z.so
# 這里直接寫三方庫的名稱是因為它是在ndk中,已在鏈接尋址路徑中,無需額外聲明
target_link_libraries(entry PUBLIC libace_napi.z.so)
3、2 externalNativeOptions
??模塊級build-profile.json5中externalNativeOptions參數(shù)是NDK工程C/C++文件編譯配置的入口,可以通過path指定CMake腳本路徑、arguments配置CMake參數(shù)、cppFlags配置C++編譯器參數(shù)、abiFilters配置編譯架構(gòu)等。
3、3 畢昇編譯器
??畢昇編譯器是基于LLVM開源軟件開發(fā)的一款用于C/C++等語言的native編譯器,能將C/C++代碼工程編譯鏈接成可以在設備上運行的二進制。在無需改動用戶代碼的條件下,相比業(yè)界主流的開源LLVM或GCC編譯器,畢昇編譯器能提供更強大的優(yōu)化能力,使編譯鏈接出來的二進制的運行時長更短、指令數(shù)更少,幫助提升應用在設備上的運行流暢度。
??開發(fā)者可以獲取DevEco Studio 5.0.3.402及以上的版本,在HarmonyOS應用的工程級build-profile.json5中簡單配置即可使用畢昇編譯器。在runtimeOS為HarmonyOS的時候,設置nativeCompiler為BiSheng,即可使用畢昇編譯器構(gòu)建HarmonyOS工程的C/C++代碼。
四、Node-API實現(xiàn)跨語言交互開發(fā)流程
4、1 導入so
??創(chuàng)建完ndk項目后,打開index.ets文件,在index.ets文件的頂部會有如下代碼,這就是導入so。
import testNapi from 'libentry.so';
4、2 注冊模塊
??當導入so,會加載其對應的so。加載so時,首先會調(diào)用napi_module_register
方法,將模塊注冊到系統(tǒng)中,并調(diào)用模塊初始化函數(shù)。如下代碼,napi_module_register
函數(shù)是在RegisterDemoModule
函數(shù)中調(diào)用,RegisterDemoModule
函數(shù)被__attribute__((constructor))
修飾。__attribute__((constructor))
是GCC和兼容的編譯器中的一個特性,用于指示編譯器將一個函數(shù)標記為在程序啟動時自動執(zhí)行的初始化函數(shù),被__attribute__((constructor))
修飾的函數(shù)會在main函數(shù)執(zhí)行之前執(zhí)行。
??napi_module_register
函數(shù)的參數(shù)是個napi_module
結(jié)構(gòu)體指針,napi_module
有兩個關(guān)鍵屬性:一個是.nm_register_func
,定義模塊初始化函數(shù);另一個是.nm_modname
,定義模塊的名稱,也就是ArkTS側(cè)引入的so庫的名稱,模塊系統(tǒng)會根據(jù)此名稱來區(qū)分不同的so。
// 準備模塊加載相關(guān)信息,將上述Init函數(shù)與本模塊名等信息記錄下來。
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = nullptr,
.reserved = {0},
};
// 加載so時,該函數(shù)會自動被調(diào)用,將上述demoModule模塊注冊到系統(tǒng)中。
extern "C" __attribute__((constructor)) void RegisterDemoModule() {
napi_module_register(&demoModule);
}
4、3模塊初始化
??如下代碼,Init
函數(shù)是在napi_module
結(jié)構(gòu)體的.nm_register_func
屬性指定的。napi_property_descriptor
結(jié)構(gòu)體用于實現(xiàn)ArkTS接口與C++接口的綁定和映射。當創(chuàng)建完ndk項目,index.d.ts文件會有export const add: (a: number, b: number) => number;
這樣一段代碼,這段代碼聲明了一個add
函數(shù),表明ArkTS會通過add
函數(shù)調(diào)用到C/C++。C/C++不能直接使用add
函數(shù),就需要使用napi_property_descriptor
結(jié)構(gòu)體將ArkTS的add
函數(shù)與C/C++函數(shù)進行綁定,這里綁定到C/C++的Add
函數(shù)。
EXTERN_C_START
// 模塊初始化
static napi_value Init(napi_env env, napi_value exports)
{
// ArkTS接口與C++接口的綁定和映射,
napi_property_descriptor desc[] = {
// ArkTS的add函數(shù)綁定C/C++的Add函數(shù)
{ "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr }
};
// 在exports對象上掛載CallNative/NativeCallArkTS兩個Native方法
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
// 模塊基本信息
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = ((void*)0),
.reserved = { 0 },
};
??大部分情況下,不僅僅需要綁定一個函數(shù)。當需要綁定多個函數(shù),首先需要index.d.ts文件聲明函數(shù),如下代碼,index.d.ts文件聲明兩個函數(shù)。
export const add: (a: number, b: number) => number;
export const sub: (a: number, b: number) => number;
??Init
函數(shù)就需要加上{ "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr }
和{ "sub", nullptr, Sub, nullptr, nullptr, nullptr, napi_default, nullptr }
static napi_value Init(napi_env env, napi_value exports)
{
// ArkTS接口與C++接口的綁定和映射,
napi_property_descriptor desc[] = {
// ArkTS的add函數(shù)綁定C/C++的Add函數(shù)
{ "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr }
// ArkTS的sub函數(shù)綁定C/C++的Sub函數(shù)
{ "sub", nullptr, Sub, nullptr, nullptr, nullptr, napi_default, nullptr }
};
// 在exports對象上掛載CallNative/NativeCallArkTS兩個Native方法
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
4、4 解析參數(shù)
??當ArkTS的add
函數(shù)綁定到C/C++的Add
函數(shù),我們需要定義Add
函數(shù)。接下來就是通用的解析流程了。
- 聲明參數(shù)的個數(shù),由于ArkTS傳入多少個參數(shù),參數(shù)的個數(shù)就多少。
- 調(diào)用
napi_get_cb_info
函數(shù)將ArkTS傳入的參數(shù)并依次放入?yún)?shù)數(shù)組中。 - 依次獲取ArkTS傳入的參數(shù)。
- 需要給ArkTS回傳參數(shù),定義napi_value,獲取到ArkTS傳入的兩個參數(shù),執(zhí)行相應的業(yè)務邏輯,將結(jié)果賦值給napi_value。
- 返回napi_value給ArkTS。
static napi_value Add(napi_env env, napi_callback_info info)
{
// 聲明參數(shù)的個數(shù),由于ArkTS會傳遞過來兩個參數(shù),參數(shù)的個數(shù)就為2
size_t argc = 2;
// 聲明參數(shù)數(shù)組
napi_value args[2] = {nullptr};
// 將ArkTS傳入的參數(shù)并依次放入?yún)?shù)數(shù)組中
napi_get_cb_info(env, info, &argc, args , nullptr, nullptr);
// 依次獲取參數(shù)
// 獲取ArkTS傳入的第一個參數(shù)
double value0;
napi_get_value_double(env, args[0], &value0);
// 獲取ArkTS傳入的第二個參數(shù)
double value1;
napi_get_value_double(env, args[1], &value1);
// 需要給ArkTS回傳參數(shù),定義napi_value
napi_value sum;
// 獲取到ArkTS傳入的兩個參數(shù)后,將兩個參數(shù)相加,相加結(jié)果賦值給napi_value
napi_create_double(env, value0 + value1, &sum);
// 返回napi_value給ArkTS
return sum;
}
五、Node-API的約束限制
5、1 so命名規(guī)則
??導入使用的模塊名和注冊時的模塊名大小寫保持一致,如模塊名為entry,則so的名字為libentry.so,napi_module中nm_modname字段應為entry,ArkTS側(cè)使用時寫作:import xxx from 'libentry.so'。
5、2注冊建議
- nm_register_func對應的函數(shù)(如上述Init函數(shù))需要加上static,防止與其他so里的符號沖突;
- 模塊注冊的入口,即使用attribute((constructor))修飾的函數(shù)的函數(shù)名(如上述RegisterDemoModule函數(shù))需要確保不與其它模塊重復。
5、3多線程限制
??每個引擎實例對應一個JS線程,實例上的對象不能跨線程操作,否則會引起應用崩潰。使用時需要遵循如下原則:
- Node-API接口只能在JS線程使用。
- Native接口入?yún)nv與特定JS線程綁定只能在創(chuàng)建時的線程使用。
六、總結(jié)
- 當ArkTS側(cè)在導入Native模塊時,ArkTS引擎會加載模塊對應的so及其依賴。首次加載時會觸發(fā)模塊的注冊,將模塊定義的方法屬性掛載到exports對象上并返回該對象。
- 當ArkTS側(cè)通過導入返回的對象調(diào)用方法時,ArkTS引擎會調(diào)用對應的C/C++方法。在C/C++函數(shù)中解析js傳遞過來的參數(shù),執(zhí)行對應的業(yè)務代碼,最后將結(jié)果返回給js。