鴻蒙NDK開發(fā)入門

一、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++。


創(chuàng)建NDK工程

??與ArkTS項目相比,NDK項目多了一個cpp目錄。


ndk目錄
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。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容