Native API在HarmonyOS应用工程中的使用指导

HarmonyOS的应用必须用js来桥接native。需要使用ace_napi仓中提供的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线程中执行。

java 复制代码
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 模块实现了数据的保存、获取、删除、清除功能。

接口声明

javascript 复制代码
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;

具体实现

完整代码参见仓下路径:OpenHarmony/arkui_napi仓库sample/native_module_storage/

1、模块注册

如下,注册了4个同步接口(getSync、setSync、removeSync、clearSync)、4个异步接口(get、set、remove、clear)。

scss 复制代码
/***********************************************
 * 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 中获取数据后,创建一个字符串对象并返回。

ini 复制代码
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。

ini 复制代码
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示例代码

javascript 复制代码
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 模块实现了一个网络服务。

接口声明

javascript 复制代码
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;
}

具体实现

完整代码参见:OpenHarmony/arkui_napi仓库sample/native_module_netserver/

1、模块注册

scss 复制代码
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 对象

arduino 复制代码
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, nullptr, nullptr, &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;
        },
        nullptr, nullptr);

    return thisVar;
}

3、从 JS 对象中取出 C++ 对象

ini 复制代码
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事件。

c 复制代码
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 为例

ini 复制代码
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示例代码

javascript 复制代码
import { NetServer } from '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线程中才能执行,否则会出现崩溃。

具体实现

完整代码参见:OpenHarmony/arkui_napi仓库sample/native_module_callback/

1、模块注册

如下,注册了1个接口test,会传入一个参数,类型为包含一个参数的函数。

markdown 复制代码
/***********************************************
 * 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线程

ini 复制代码
#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示例代码

javascript 复制代码
import callback from 'libcallback.so';

export default {
  testcallback() {
      callback.test((data) => {
      console.error('test result = ' + data)
    })
  }
}
相关推荐
JavaPub-rodert26 分钟前
鸿蒙生态崛起:开发者的机遇与挑战
华为·harmonyos
yilylong4 小时前
鸿蒙(Harmony)实现滑块验证码
华为·harmonyos·鸿蒙
baby_hua4 小时前
HarmonyOS第一课——DevEco Studio的使用
华为·harmonyos
HarmonyOS_SDK4 小时前
融合虚拟与现实,AR Engine为用户提供沉浸式交互体验
harmonyos
- 羊羊不超越 -6 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
长弓三石7 小时前
鸿蒙网络编程系列44-仓颉版HttpRequest上传文件示例
前端·网络·华为·harmonyos·鸿蒙
SameX9 小时前
鸿蒙 Next 电商应用安全支付与密码保护实践
前端·harmonyos
SuperHeroWu710 小时前
【HarmonyOS】键盘遮挡输入框UI布局处理
华为·harmonyos·压缩·keyboard·键盘遮挡·抬起
sanzk15 小时前
华为鸿蒙应用开发
华为·harmonyos
SoraLuna19 小时前
「Mac畅玩鸿蒙与硬件28」UI互动应用篇5 - 滑动选择器实现
macos·ui·harmonyos