HarmonyOS NEXT應用開發之使用AKI輕鬆實現跨語言呼叫

生活就是这么怪發表於2024-04-18

介紹

針對JS與C/C++跨語言訪問場景,NAPI使用比較繁瑣。而AKI提供了極簡語法糖使用方式,一行程式碼完成JS與C/C++的無障礙跨語言互調,使用方便。本示例將介紹使用AKI編寫C++跨執行緒呼叫JS函式場景。透過呼叫C++全域性函式,建立子執行緒來呼叫JS函式,實現對變數value的加10操作,為開發者使用AKI提供參考。

效果圖預覽

image

使用說明

  1. 點選頁面“AKI跨執行緒呼叫JS函式”按鈕,每次點選,顯示數值加10。

實現思路

以下是使用AKI和NPAI的libuv實現跨執行緒呼叫JS函式的實現對比:

  1. AKI和NAPI初始化。

    AKI初始化使用JSBIND_ADDON註冊Native外掛,使用AKI的JSBIND_GLOBAL註冊FFI特性,然後在JSBIND_GLOBAL作用域下使用AKI的JSBIND_FUNCTION繫結C++全域性函式AkiThreadsCallJs。原始碼參考akiusepractice.cpp

...
// 使用JSBIND_ADDON註冊Native外掛,可從JavaScript import匯入外掛。註冊AKI外掛名:即為編譯*.so名稱,規則與NAPI一致。
JSBIND_ADDON(aki_use_practice)
// 使用JSBIND_GLOBAL註冊FFI特性。用於圈定需要繫結的全域性函式作用域。
JSBIND_GLOBAL() {
  // 在JSBIND_GLOBAL作用域下使用JSBIND_FUNCTION繫結C++全域性函式後,可從JavaScript直接呼叫。
  JSBIND_FUNCTION(AkiThreadsCallJs);
}
...

NAPI的libuv初始化需要定義napi_property_descriptor結構體,準備模組載入相關資訊,將Init函式與模組名等資訊記錄下來。原始碼參考hello.cpp

...
static napi_value Init(napi_env env, napi_value exports)
{
    // 第一個引數"add"為ArkTS側對應方法的名稱。
    napi_property_descriptor desc[] = {
        {"UvWorkTest", nullptr, UvWorkTest, nullptr, nullptr, nullptr, napi_default, nullptr}
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}

// 準備模組載入相關資訊,將Init函式與本模組名等資訊記錄下來。
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},
};

extern "C" __attribute__((constructor)) void RegisterModule(void) { napi_module_register(&demoModule); } 
  1. AKI和NAPI在native側的業務函式實現。

    AKI在native側業務函式實現是在AkiThreadsCallJs中建立子執行緒,子執行緒中使用aki::JSBind:: GetJSFunction獲取指定JavaScript函式akiAccumulate的控制代碼後,使用Invoke觸發呼叫。原始碼參考akiusepractice.cpp

// 定義C++函式AkiThreadsCallJs。從native主執行緒中建立子執行緒subThread呼叫JavaScript函式。
void AkiThreadsCallJs(int value) {
    // 建立子執行緒subThread
    std::thread subThread([=]() {
        // 使用aki::JSBind::GetJSFunction獲取指定JavaScript函式控制代碼後,使用Invoke觸發呼叫。這裡獲取JS側定義的函式akiAccumulate。
        if (auto func = aki::JSBind::GetJSFunction("akiAccumulate")) {
            // 定義一個函式物件callback,該函式物件接受一個整數引數並返回void。
            std::function<void(int)> callback = [](int value) {};
            // 呼叫JavaScript函式,Invoke<T>指定返回值型別。
            func->Invoke<void>(value, callback);
        }
    });
    // 將子執行緒subThread從主執行緒中分離出來,獨立執行。
    subThread.detach();
    return;
}

NAPI的libuv在native側業務函式實現是在native主執行緒中實現UvWorkTest介面。介面接收到ArkTS傳入的JS回撥函式後建立子執行緒,執行函式CallbackUvWorkTest。在CallbackUvWorkTest中建立工作任務workReq,透過uv_queue_work將工作任務新增到libuv佇列中,等待被執行。原始碼參考hello.cpp

static napi_value UvWorkTest(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value argv[1] = {0};
    napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);

    napi_valuetype valueType = napi_undefined;
    napi_typeof(env, argv[0], &valueType);
    if (valueType != napi_function) {
        OH_LOG_ERROR(LOG_APP, "UvWorkTest param is not function");
        return nullptr;
    }

    OH_LOG_INFO(LOG_APP, "UvWorkTest current value:[%{public}d]", g_cValue);
    for (int i = 0; i < g_threadNum; i++) {
        auto asyncContext = new CallbackContext();
        if (asyncContext == nullptr) {
            OH_LOG_ERROR(LOG_APP, "UvWorkTest new asyncContext fail!");
            return nullptr;
        }
        asyncContext->env = env;
        asyncContext->retData = i;
        OH_LOG_INFO(LOG_APP, "UvWorkTest thread begin index:[%{public}d], value:[%{public}d]", i, g_cValue);
        napi_create_reference(env, argv[0], 1, &asyncContext->callbackRef);
        std::thread testThread(CallbackUvWorkTest, asyncContext);
        testThread.detach();
        OH_LOG_INFO(LOG_APP, "UvWorkTest thread end index:[%{public}d], value:[%{public}d]", i, g_cValue);
    }
    return nullptr;
}

void CallbackUvWorkTest(CallbackContext *context) {
    if (context == nullptr) {
        OH_LOG_ERROR(LOG_APP, "UvWorkTest context is nullptr");
        return;
    }
    uv_loop_s *loop = nullptr;
    napi_get_uv_event_loop(context->env, &loop);
    // 建立工作資料結構,自定義資料結構新增在data中
    uv_work_t *workReq = new uv_work_t;
    if (workReq == nullptr) {
        if (context != nullptr) {
            napi_delete_reference(context->env, context->callbackRef);
            delete context;
            OH_LOG_INFO(LOG_APP, "UvWorkTest delete context");
            context = nullptr;
        }
        OH_LOG_ERROR(LOG_APP, "UvWorkTest new uv_work_t fail!");
        return;
    }
    workReq->data = (void *)context;
    // 此列印位於子執行緒
    OH_LOG_INFO(LOG_APP, "UvWorkTest childThread_1 [%{public}d]", g_cValue);
    // 新增工作任務到libuv的佇列中
    uv_queue_work(loop, workReq, WorkCallback, AfterWorkCallback);
}
  1. AKI和NAPI在ArkTS側呼叫JS函式。

    AKI在ArkTS側使用AKI的JSBind.bindFunction繫結JavaScript全域性函式akiAccumulate。使用AKI呼叫C++全域性函式AkiThreadsCallJs。原始碼參考AkiView.ets

...
// 使用AKI的JSBind.bindFunction繫結JavaScript全域性函式。
libaki.JSBind.bindFunction("akiAccumulate", (values: number) => {
  // 對變數value做加10操作,重新整理Text元件的value值。
  values += 10;
  this.value = values;
});
// TODO:知識點:使用AKI呼叫C++全域性函式AkiThreadsCallJs,並傳入引數value。
libaki.AkiThreadsCallJs(this.value);
...

NAPI的libuv在ArkTS側呼叫C++全域性函式UvWorkTest。原始碼參考Index.ets

entry.UvWorkTest((values: number) => {
  values += 10;
  logger.info('UvWorkTest js callback value = ', values.toString());
  this.value = values;
  return values;
}

透過以上AKI和NAPI實現跨執行緒呼叫JS的實現步驟的對比,可以發現AKI在native側相較於NAPI實現的程式碼量要少很多,使用也更加方便。

高效能知識點

  1. AKI使用方便,但相比於NPAI,對效能的損耗相對會多一些。對效能要求不高,且更需要易用性開發的場景,推薦使用AKI。

工程結構&模組型別

akiusepractice                                 // har型別
|---src\main\cpp
|   |---akiusepractice.cpp                     // native層-native側業務處理
|   |---CMakeLists.txt                         // native層-AKI相關CMake配置
|---src\main\ets\view
|   |---AkiView.ets                            // 檢視層-AKI跨執行緒呼叫JS函式頁面 

模組依賴

  1. 本例項依賴AKI
  2. 本例項依賴common模組來實現公共元件FunctionDescription

參考資料

  1. AKI
  2. 公共元件FunctionDescription

相關文章