Native API 在 HarmonyOS 應用工程中的使用指導

HarmonyOS開發者社群發表於2023-12-05

HarmonyOS 的應用必須用 js 來橋接 native。需要使用 倉中提供的 napi 介面來處理 js 互動。napi 提供的介面名與三方 Node.js 一致,目前支援部分介面,符號表見 ace_napi 倉中的 libnapi.ndk.json 檔案。

開發流程

在 DevEco Studio 的模板工程中包含使用 Native API 的預設工程,使用 File->New->Create Project 建立 Native C++模板工程。建立後在 main 目錄下會包含 cpp 目錄,可以使用 ace_napi 倉下提供的 napi 介面進行開發。

js 側透過 import 引入 native 側包含處理 js 邏輯的 so,如:import hello from 'libhello.so',意為使用 libhello.so 的能力,native 側透過 napi 介面建立的 js 物件會給到應用 js 側的 hello 物件。

開發建議

註冊建議

●  nm_register_func 對應的函式需要加上 static,防止與其他 so 裡的符號衝突。

●  模組註冊的入口,即使用__attribute__((constructor))修飾的函式的函式名需要確保不與其他模組重複。

so 命名規則

so 命名必須符合以下規則:

●  每個模組對應一個 so。

●  如模組名為 hello,則 so 的名字為 libhello.so,napi_module 中 nm_modname 欄位應為 hello,大小寫與模組名保持一致,應用使用時寫作:import hello from 'libhello.so'。

js 物件執行緒限制

ark 引擎會對 js 物件執行緒使用進行保護,使用不當會引起應用 crash,因此需要遵循如下原則:

●  napi 介面只能在 js 執行緒使用。

●  env 與執行緒繫結,不能跨執行緒使用。native 側 js 物件只能在建立時的執行緒使用,即與執行緒所持有的 env 繫結。

標頭檔案引入限制

在使用 napi 的物件和方法時需要引用"napi/native_api.h"。否則在只引入三方庫標頭檔案時,會出現介面無法找到的編譯報錯。

napi_create_async_work 介面說明

napi_create_async_work 裡有兩個回撥:

●  execute:用於非同步處理業務邏輯。因為不在 js 執行緒中,所以不允許呼叫 napi 的介面。業務邏輯的返回值可以返回到 complete 回撥中處理。

●  complete:可以呼叫 napi 的介面,將 execute 中的返回值封裝成 js 物件返回。此回撥在 js 執行緒中執行。


napi_status 
napi_create_async_work
(napi_env env,
                                   napi_value async_resource,
                                   napi_value async_resource_name,
                                   napi_async_execute_callback execute,
                                   napi_async_complete_callback complete,
                                   
void* data,
                                   napi_async_work* result)





storage 模組——同步非同步介面封裝

模組簡介

本示例透過實現 storage 模組展示了同步和非同步方法的封裝。storage 模組實現了資料的儲存、獲取、刪除、清除功能。

介面宣告



import { 
AsyncCallback } 
from 
'./basic';
declare namespace storage {
  
function 
get(
key: string, callback: AsyncCallback<string>): 
void;
  
function 
get(
key: string, defaultValue: string, callback: AsyncCallback<string>): 
void;
  
function 
get(
key: string, defaultValue?: string): 
Promise<string>;
  
function 
set(
key: string, value: string, callback: AsyncCallback<string>): 
void;
  
function 
remove(
key: string, callback: AsyncCallback<
void>): 
void;
  
function 
clear(
callback: AsyncCallback<
void>): 
void;
  
function 
getSync(
key: string, defaultValue?: string): string;
  
function 
setSync(
key: string, value: string): 
void;
  
function 
removeSync(
key: string): 
void;
  
function 
clearClear(): 
void;
}

export 
default storage;




具體實現

完整程式碼參見倉下路徑: 倉庫 sample/native_module_storage/

1、模組註冊

如下,註冊了 4 個同步介面(getSync、setSync、removeSync、clearSync)、4 個非同步介面(get、set、remove、clear)。



/***********************************************
 * Module export and register
 ***********************************************/


static napi_value 
StorageExport(
napi_env env, napi_value exports)
{
    napi_property_descriptor desc[] = {
        DECLARE_NAPI_FUNCTION(
"get", JSStorageGet),
        DECLARE_NAPI_FUNCTION(
"set", JSStorageSet),
        DECLARE_NAPI_FUNCTION(
"remove", JSStorageDelete),
        DECLARE_NAPI_FUNCTION(
"clear", JSStorageClear),


       DECLARE_NAPI_FUNCTION( "getSync", JSStorageGetSync),        DECLARE_NAPI_FUNCTION( "setSync", JSStorageSetSync),        DECLARE_NAPI_FUNCTION( "deleteSync", JSStorageDeleteSync),        DECLARE_NAPI_FUNCTION( "clearSync", JSStorageClearSync),    };    NAPI_CALL(env, napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[ 0]), desc));     return exports; }
// storage module static napi_module storage_module = {.nm_version = 1,                                     .nm_flags = 0,                                     .nm_filename = nullptr,                                     .nm_register_func = StorageExport,                                     .nm_modname = "storage",                                     .nm_priv = (( void*) 0),                                     .reserved = { 0}};
// storage module register extern "C" __attribute__((constructor)) void StorageRegister() {    napi_module_register(&storage_module); }



2、getSync 函式實現

如上註冊時所寫,getSync 對應的函式是 JSStorageGetSync 。從 gKeyValueStorage 中獲取資料後,建立一個字串物件並返回。



static napi_value 
JSStorageGetSync
(napi_env env, napi_callback_info info)
{
    GET_PARAMS(env, info, 
2);
    NAPI_ASSERT(env, argc >= 
1, 
"requires 1 parameter");
    
char key[
32] = {
0};
    
size_t 
keyLen 
= 
0;
    
char value[
128] = {
0};
    
size_t 
valueLen 
= 
0;


    // 引數解析     for ( size_t i = 0; i < argc; i++) {        napi_valuetype valueType;        napi_typeof(env, argv[i], &valueType);
        if (i == 0 && valueType == napi_string) {            napi_get_value_string_utf8(env, argv[i], key, 31, &keyLen);        } else if (i == 1 && valueType == napi_string) {            napi_get_value_string_utf8(env, argv[i], value, 127, &valueLen);             break;        } else {            NAPI_ASSERT(env, false, "type mismatch");        }    }
    // 獲取資料的業務邏輯,這裡簡單地從一個全域性變數中獲取     auto itr = gKeyValueStorage.find(key);     napi_value result = nullptr;     if (itr != gKeyValueStorage.end()) {         // 獲取到資料後建立一個string型別的JS物件        napi_create_string_utf8(env, itr->second.c_str(), itr->second.length(), &result);    } else if (valueLen > 0) {         // 沒有獲取到資料使用預設值建立JS物件        napi_create_string_utf8(env, value, valueLen, &result);    } else {        NAPI_ASSERT(env, false, "key does not exist");    }     // 返回結果     return result; }



3、get 函式實現

如上註冊時所寫,get 對應的函式式 JSStorageGet。



static napi_value JSStorageGet(napi_env env, napi_callback_info info)
{
    GET_PARAMS(env, info, 
3);
    NAPI_ASSERT(env, argc >= 
1"requires 1 parameter");


     // StorageAsyncContext是自己定義的一個類,用於儲存執行過程中的資料     StorageAsyncContext* asyncContext =  new StorageAsyncContext();
    asyncContext->env = env;
     // 獲取引數      for (size_t i =  0; i < argc; i++) {         napi_valuetype valueType;         napi_typeof(env, argv[i], &valueType);
         if (i ==  0 && valueType == napi_string) {             napi_get_value_string_utf8(env, argv[i], asyncContext->key,  31, &asyncContext->keyLen);         }  else  if (i ==  1 && valueType == napi_string) {             napi_get_value_string_utf8(env, argv[i], asyncContext->value,  127, &asyncContext->valueLen);         }  else  if (i ==  1 && valueType == napi_function) {             napi_create_reference(env, argv[i],  1, &asyncContext->callbackRef);              break;         }  else  if (i ==  2 && valueType == napi_function) {             napi_create_reference(env, argv[i],  1, &asyncContext->callbackRef);         }  else {             NAPI_ASSERT(env,  false"type mismatch");         }     }
    napi_value result = nullptr;
     // 根據引數判斷開發者使用的是promise還是callback      if (asyncContext->callbackRef == nullptr) {          // 建立promise         napi_create_promise(env, &asyncContext->deferred, &result);     }  else {         napi_get_undefined(env, &result);     }
    napi_value resource = nullptr;     napi_create_string_utf8(env,  "JSStorageGet", NAPI_AUTO_LENGTH, &resource);
    napi_create_async_work(         env, nullptr, resource,          // 回撥1:此回撥由napi非同步執行,裡面就是需要非同步執行的業務邏輯。由於是非同步執行緒執行,所以不要在此透過napi介面操作JS物件。         [](napi_env env,  void* data) {             StorageAsyncContext* asyncContext = (StorageAsyncContext*)data;             auto itr = gKeyValueStorage.find(asyncContext->key);              if (itr != gKeyValueStorage.end()) {                 strncpy_s(asyncContext->value,  127, itr->second.c_str(), itr->second.length());                 asyncContext->status =  0;             }  else {                 asyncContext->status =  1;             }         },          // 回撥2:此回撥在上述非同步回撥執行完後執行,此時回到了JS執行緒來回撥開發者傳入的回撥         [](napi_env env, napi_status status,  void* data) {             StorageAsyncContext* asyncContext = (StorageAsyncContext*)data;             napi_value result[ 2] = { 0};              if (!asyncContext->status) {                 napi_get_undefined(env, &result[ 0]);                 napi_create_string_utf8(env, asyncContext->value, strlen(asyncContext->value), &result[ 1]);             }  else {                 napi_value message = nullptr;                 napi_create_string_utf8(env,  "key does not exist", NAPI_AUTO_LENGTH, &message);                 napi_create_error(env, nullptr, message, &result[ 0]);                 napi_get_undefined(env, &result[ 1]);             }              if (asyncContext->deferred) {                  // 如果走的是promise,那麼判斷回撥1的結果                  if (!asyncContext->status) {                      // 回撥1執行成功(status為1)時觸發,也就是觸發promise裡then裡面的回撥                     napi_resolve_deferred(env, asyncContext->deferred, result[ 1]);                 }  else {                      // 回撥1執行失敗(status為0)時觸發,也就是觸發promise裡catch裡面的回撥                     napi_reject_deferred(env, asyncContext->deferred, result[ 0]);                 }             }  else {                  // 如果走的是callback,則透過napi_call_function呼叫callback回撥返回結果                 napi_value callback = nullptr;                 napi_value returnVal;                 napi_get_reference_value(env, asyncContext->callbackRef, &callback);                 napi_call_function(env, nullptr, callback,  2, result, &returnVal);                 napi_delete_reference(env, asyncContext->callbackRef);             }             napi_delete_async_work(env, asyncContext->work);             delete asyncContext;         },         ( void*)asyncContext, &asyncContext->work);     napi_queue_async_work(env, asyncContext->work);
     return result; }



4、js 示例程式碼



import storage 
from 
'libstorage.so';


export  default {   testGetSync() {       const name = storage. getSync( 'name');      console. log( 'name is ' + name);  },   testGet() {     storage. get( 'name')    . then( date => {          console. log( 'name is ' + data);    })    . catch( error => {          console. log( 'error: ' + error);    });  } }


NetServer 模組——native 與 js 物件繫結

模組簡介

本示例展示了 on/off/once 訂閱方法的實現,同時也包含了 C++ 與 js 物件透過 wrap 介面的繫結。NetServer 模組實現了一個網路服務。

介面宣告



export 
class 
NetServer {
  
function 
start(
port: number): 
void;
  
function 
stop(): 
void;
  
function 
on(

'start' | 
'stop', callback: 
Function): 
void;
  
function 
once(

'start' | 
'stop', callback: 
Function): 
void;
  
function 
off(

'start' | 
'stop', callback: 
Function): 
void;
}




具體實現

完整程式碼參見: 倉庫 sample/native_module_netserver/

1、模組註冊



static napi_value NetServer::Export(napi_env env, napi_value 
exports)
{
    const 
char className[] = 
"NetServer";
    napi_property_descriptor properties[] = {
        DECLARE_NAPI_FUNCTION(
"start", JS_Start),
        DECLARE_NAPI_FUNCTION(
"stop", JS_Stop),
        DECLARE_NAPI_FUNCTION(
"on", JS_On),
        DECLARE_NAPI_FUNCTION(
"once", JS_Once),
        DECLARE_NAPI_FUNCTION(
"off", JS_Off),
    };
    
napi_value 
netServerClass 
= nullptr;


   napi_define_class(env, className, sizeof(className), JS_Constructor, nullptr, countof(properties), properties,                      &netServerClass);
   napi_set_named_property(env,  exports, "NetServer", netServerClass);
    return  exports; }



2、在建構函式中繫結 C++ 與 JS 物件



napi_value 
NetServer::JS_Constructor
(napi_env env, napi_callback_info cbinfo)
{
    napi_value thisVar = 
nullptr;
    
void* data = 
nullptr;
    
napi_get_cb_info(env, cbinfo, 
nullptrnullptr, &thisVar, &data);


    // C++ Native物件,準備與JS物件對映在一起    NetServer* netServer =  new  NetServer(env, thisVar);
    // 使用napi_wrap將netServer與thisVar(即當前建立的這個JS物件)做繫結     napi_wrap(         env, thisVar, netServer,         // JS物件由引擎自動回收釋放,當JS物件釋放時觸發該回撥,在改回撥中釋放netServer        [](napi_env env,  void* data,  void* hint) {             printf( "NetServer::Destructor\n");            NetServer* netServer = (NetServer*)data;              delete netServer;        },          nullptrnullptr);
    return thisVar; }



3、從 JS 物件中取出 C++ 物件



napi_value 
NetServer::JS_Start
(napi_env env, napi_callback_info cbinfo)
{
    
size_t argc = 
1;
    napi_value argv[
1] = {
0};
    napi_value thisVar = 
nullptr;
    
void* data = 
nullptr;
    
napi_get_cb_info(env, cbinfo, &argc, argv, &thisVar, &data);


   NetServer* netServer =  nullptr;     // 透過napi_unwrap從thisVar中取出C++物件     napi_unwrap(env, thisVar, ( void**)&netServer);
    NAPI_ASSERT(env, argc >= 1, "requires 1 parameter");
    napi_valuetype valueType;     napi_typeof(env, argv[ 0], &valueType);     NAPI_ASSERT(env, valueType == napi_number, "type mismatch for parameter 1");
     int32_t port = 0;     napi_get_value_int32(env, argv[ 0], &port);
    // 開啟服務     netServer-> Start(port);
    napi_value result =  nullptr;     napi_get_undefined(env, &result);     return result; }



netServer->Start 後回撥透過 on 註冊的 start 事件。




int 
NetServer::Start
(
int port)
{
    
printf(
"NetServer::Start thread_id: %ld \n", 
uv_thread_self());


    struct sockaddr_in addr;      int r;
    uv_ip4_addr( "0.0.0.0", port, &addr);
    r = uv_tcp_init(loop_, &tcpServer_);     if (r) {         fprintf(stderr, "Socket creation error\n");         return 1;    }
    r = uv_tcp_bind(&tcpServer_, ( const struct sockaddr*)&addr, 0);     if (r) {         fprintf(stderr, "Bind error\n");         return 1;    }
    r = uv_listen(( uv_stream_t*)&tcpServer_, SOMAXCONN, OnConnection);     if (r) {         fprintf(stderr, "Listen error %s\n", uv_err_name(r));         return 1;    }
    // 服務啟動後觸發“start”事件     Emit( "start"nullptr);
    return 0; }



4、註冊或釋放(on/off/once)事件,以 on 為例



napi_value 
NetServer::JS_On
(napi_env env, napi_callback_info cbinfo)
{
    
size_t argc = 
2;
    napi_value argv[
2] = {
0};
    napi_value thisVar = 
0;
    
void* data = 
nullptr;
    
napi_get_cb_info(env, cbinfo, &argc, argv, &thisVar, &data);


   NetServer* netServer =  nullptr;     // 透過napi_unwrap取出NetServer指標     napi_unwrap(env, thisVar, ( void**)&netServer);
    NAPI_ASSERT(env, argc >= 2, "requires 2 parameter");
    // 引數型別校驗     napi_valuetype eventValueType;     napi_typeof(env, argv[ 0], &eventValueType);     NAPI_ASSERT(env, eventValueType == napi_string, "type mismatch for parameter 1");
    napi_valuetype eventHandleType;     napi_typeof(env, argv[ 1], &eventHandleType);     NAPI_ASSERT(env, eventHandleType == napi_function, "type mismatch for parameter 2");
    char type[ 64] = { 0};      size_t typeLen = 0;
    napi_get_value_string_utf8(env, argv[ 0], type, 63, &typeLen);
    // 註冊事件handler     netServer-> On(( const char*)type, argv[ 1]);
    napi_value result =  nullptr;     napi_get_undefined(env, &result);     return result; }



5、js 示例程式碼



import { 
NetServerfrom 
'libnetserver.so';


export  default {   testNetServer() {        var netServer =  new  NetServer();       netServer. on( 'start', ( event) => {});       netServer. start( 1000); // 埠號1000, start完成後回撥上面註冊的 “start” 回撥  } }


在非 JS 執行緒中回撥 JS 介面

模組簡介

本示例介紹如何在非 JS 執行緒中回撥 JS 應用的回撥函式。例如 JS 應用中註冊了某個 sensor 的監聽,這個 sensor 的資料是由一個 SA 服務來上報的,當 SA 透過 IPC 調到客戶端時,此時的執行執行緒是一個 IPC 通訊執行緒,與應用的 JS 執行緒是兩個不同的執行緒。這時就需要將執行 JS 回撥的任務拋到 JS 執行緒中才能執行,否則會出現崩潰。

具體實現

完整程式碼參見: 倉庫 sample/native_module_callback/

1、模組註冊

如下,註冊了 1 個介面 test,會傳入一個引數,型別為包含一個引數的函式。



/***********************************************
 * Module export and register
 ***********************************************/


static napi_value 
CallbackExport(
napi_env env, napi_value exports)
{
    
static napi_property_descriptor desc[] = {
        DECLARE_NAPI_FUNCTION(
"test", JSTest)
    };
    NAPI_CALL(env, napi_define_properties(env, exports, 
sizeof(desc) / 
sizeof(desc[
0]), desc));
    
return exports;
}


// callback module define static napi_module callbackModule = {    .nm_version = 1,    .nm_flags = 0,    .nm_filename = nullptr,    .nm_register_func = CallbackExport,    .nm_modname = "callback",    .nm_priv = (( void*) 0),    .reserved = { 0 }, };
// callback module register extern "C" __attribute__((constructor))  void  CallbackTestRegister() {    napi_module_register(&callbackModule); }



2、獲取 env 中的 loop,拋任務回 JS 執行緒



#
include 
<thread>


# include  "napi/native_api.h" # include  "napi/native_node_api.h"
# include  "uv.h"
struct CallbackContext {     napi_env env =  nullptr;     napi_ref callbackRef =  nullptr;      int retData = 0; };
void  callbackTest (CallbackContext* context) {     uv_loop_s* loop =  nullptr;     // 此處的env需要在註冊JS回撥時儲存下來。從env中獲取對應的JS執行緒的loop。     napi_get_uv_event_loop(context->env, &loop);         // 建立uv_work_t用於傳遞私有資料,注意回撥完成後需要釋放記憶體,此處省略生成回傳資料的邏輯,傳回int型別1。      uv_work_t* work =  new  uv_work_t;     context->retData = 1;     work->data = ( void*)context;         // 呼叫libuv介面拋JS任務到loop中執行。     uv_queue_work(        loop,         work,         // 此回撥在另一個普通執行緒中執行,用於處理非同步任務,回撥執行完後執行下面的回撥。本場景下該回撥不需要執行任務。        []( uv_work_t* work) {},         // 此回撥會在env對應的JS執行緒中執行。        []( uv_work_t* work,  int status) {            CallbackContext* context = (CallbackContext*)work->data;             napi_handle_scope scope =  nullptr;             // 開啟handle scope用於管理napi_value的生命週期,否則會記憶體洩露。             napi_open_handle_scope(context->env, &scope);             if (scope ==  nullptr) {                 return;            }
            // 呼叫napi。             napi_value callback =  nullptr;             napi_get_reference_value(context->env, context->callbackRef, &callback);             napi_value retArg;             napi_create_int32(context->env, context->retData, &retArg);             napi_value ret;             napi_call_function(context->env,  nullptr, callback, 1, &retArg, &ret);             napi_delete_reference(context->env, context->callbackRef);
            // 關閉handle scope釋放napi_value。             napi_close_handle_scope(context->env, scope);
            // 釋放work指標。             if (work !=  nullptr) {                  delete work;            }
             delete context;        }    ); }
static napi_value  JSTest (napi_env env, napi_callback_info info) {      size_t argc = 1;     napi_value argv[ 1] = { 0 };     napi_value thisVar =  nullptr;      void* data =  nullptr;     napi_get_cb_info(env, info, &argc, argv, &thisVar, &data);
    // 獲取第一個入參,即需要後續觸發的回撥函式     napi_valuetype valueType = napi_undefined;     napi_typeof(env, argv[ 0], &valueType);     if (valueType != napi_function) {         return  nullptr;    }     // 存下env與回撥函式,用於傳遞      auto asyncContext =  new  CallbackContext();     asyncContext->env = env;     napi_create_reference(env, argv[ 0], 1, &asyncContext->callbackRef);     // 模擬拋到非js執行緒執行邏輯     std::thread  testThread (callbackTest, asyncContext);     testThread. detach();
    return  nullptr; }



3、  js 示例程式碼



import callback 
from 
'libcallback.so';


export  default {   testcallback() {       callback. test( ( data) => {        console. error( 'test result = ' + data)    })  } }


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70009402/viewspace-2998834/,如需轉載,請註明出處,否則將追究法律責任。