对象包装object-wrap

前言

JS调用N-API的数据,对于简单的数据类型,只需要N-API返回对应类型的napi_value数据即可,如下C++侧两数之和代码:

static napi_value Add(napi_env env, napi_callback_info info)
{
  ...
  // 调用napi_create_double方法把 C++类型转换成 napi_value 类型
  napi_value sum;
  napi_create_double(env, value0 + value1, &sum);
  // 返回 napi_value 类型
  return sum;
}

但是对于一些复杂的数据类型(如我们常用C++的类对象),是不能直接返回一个napi_value数据的。这时我们需要对这些数据进行一系列操作后将其导出,这样JS才能使用导出后的对象。 N-API 提供了一种包装C++ 类和实例对象的方式,以便可以在 JS侧调用C++类的构造函数和方法,参考JS侧如下代码:

import testNapi from 'libentry.so';
// JS侧对C++侧的类进行实例化
private myObject: testNapi.MyObject = new testNapi.MyObject(...params);
// 调用类的实例对象上的操作方法
myObject.operate();

回顾一下JS构造函数定义:当一个函数可以使用关键字new来创建对象时,这个函数就是构造函数。对于ES6中新增的class,其内部已经定义了构造函数constructor,用于创建类的实例对象。

C++类实例对象包装和导出的具体过程:

object_wrapper

场景一:对象包装,返回给JS侧创建实例

JS侧

实现一个sample,对象的创建分两种场景:1. 使用new关键字创建一个类实例对象,2. 使用C++提供的方法直接获取一个实例对象

对象实例化的参数为2个number类型的参数,分别点击前两个button可以创建两个场景下的实例对象,分别点击后四个button可以操作对象上的方法(对两个number参数进行加减乘除操作)。

代码如下:

import hilog from '@ohos.hilog';
import testNapi from 'libentry.so'

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  myObject: testNapi.MyObject;
  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(30)
          .fontWeight(FontWeight.Bold)

        Button('1. create js class instance by new')
          .margin({ top: 20 })
          .onClick(() => {
            this.myObject = new testNapi.MyObject(3, 4);
            this.message = `myObject = new MyObject(${this.myObject.num1}, ${this.myObject.num2})`;
          })

        Divider().margin(20).strokeWidth(3)

        Button('add operate')
          .onClick(() => {
            const ans = this.myObject.operator(0);
            this.message = `${this.myObject.num1} + ${this.myObject.num2} = ${ans}`;
          })

        Button('sub operate')
          .margin({ top: 20 })
          .onClick(() => {
            const ans = this.myObject.operator(1);
            this.message = `${this.myObject.num1} - ${this.myObject.num2} = ${ans}`;
          })

        Button('mul operate')
          .margin({ top: 20 })
          .onClick(() => {
            const ans = this.myObject.operator(2);
            this.message = `${this.myObject.num1} * ${this.myObject.num2} = ${ans}`;
          })

        Button('div operate')
          .margin({ top: 20 })
          .onClick(() => {
            const ans = this.myObject.operator(3);
            this.message = `${this.myObject.num1} / ${this.myObject.num2} = ${ans}`;
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

Native C++侧

头文件
#include "napi/native_api.h"

#ifndef NAPIObjectBindDemo_object_wrap_H
#define NAPIObjectBindDemo_object_wrap_H

class MyObject {
public:
  // 初始化函数
  static void Init(napi_env env, napi_value exports);
  // 构造函数引用
  static napi_ref constructor_ref;
private:
  explicit MyObject(double a = 0, double b = 0);
  ~MyObject();
  // 释放资源的函数(类似类的析构函数)
  static void Destructor(napi_env env, void *finalize_data, void *finalize_hint);
  // new一个新的JS对象时实际的构造函数
  static napi_value JSConstructor(napi_env env, napi_callback_info info);
  // JS调用类的方法
  static napi_value JSOperate(napi_env env, napi_callback_info info);
  
  double a;
  double b;
  napi_env env_;
  napi_ref wrapped_obj;
};

#endif // NAPIObjectBindDemo_object_wrap_H
模块注册

RegisterEntryModule() 方法内调用了NAPI 提供的模块注册方法 napi_module_register() ,表示把定义的 demoModule 模块注册到系统中。而在demoModule 内指定了入口函数为Init

MyObject::Init中,主要实现定义、导出C++类对应的JS构造函数和方法,如下:

  • 使用napi方法napi_define_class,定义JS侧实例化时使用的类(类名也为"MyObject"),指定类实例化时的构造函数为MyObject::JSConstructor,类属性和方法为classDesc,该napi方法的调用结果为JS类实例化时的构造函数,保存在变量constructor中;
  • constructor保存到napi_ref类型的变量constructor_ref中,在函数GetNewInstance中可以使用constructor_ref直接创建JS类的实例对象;
  • JS类的构造函数constructor绑定在导出对象exports上,即 exports.className = constructor,这样JS侧就可以根据构造函数创建实例对象.
/**
 * 定义并导出JS类,持久化保存JS类的构造函数
 * @param env
 * @param exports
 */
void MyObject::Init(napi_env env, napi_value exports) {
  OH_LOG_INFO(LOG_APP, "=== MyObject::Init");

  napi_property_descriptor classDesc[] = {
      {"operator", nullptr, MyObject::JSOperate, nullptr, nullptr, nullptr, napi_default, nullptr}};
  
  napi_value constructor = nullptr;
  const char *className = "MyObject";

  // 根据C++类MyObject定义JS类,JS类的构造函数保存在constructor
  napi_define_class(env, className, sizeof(className), MyObject::JSConstructor, nullptr, 1, classDesc, &constructor);

  // 将constructor保存起来,方便以后直接创建js类的实例对象
  napi_create_reference(env, constructor, 1, &constructor_ref);
  
  // 将constructor绑定在导出对象exports上,即 exports.className = constructor
  napi_set_named_property(env, exports, className, constructor);
}

EXTERN_C_START
napi_value Init(napi_env env, napi_value exports) {
  // 初始化JS类
  MyObject::Init(env, exports);
  
  napi_property_descriptor desc[] = {
      {"getNewInstance", nullptr, GetNewInstance, nullptr, nullptr, nullptr, napi_default, nullptr}};
  
  // 在导出文件index.d.ts里,getNewInstance函数没有写在类内部,需要额外绑定到exports上
  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},
};

extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
  napi_module_register(&demoModule);
}

napi_define_class

定义JS侧实例化时使用的类,指定类实例化时的构造函数,类属性和方法,该napi方法的调用结果为JS类实例化时的构造函数

// api描述
napi_status napi_define_class(napi_env env,
                              const char* utf8name,
                              size_t length,
                              napi_callback constructor,
                              void* data,
                              size_t property_count,
                              const napi_property_descriptor* properties,
                              napi_value* result);
// 根据C++类MyObject定义JS类,JS类的构造函数保存在constructor
napi_define_class(env, className, sizeof(className), MyObject::JSConstructor, nullptr, 1, classDesc, &constructor);
  • utf8name:JavaScript 构造函数的名称;
  • length:utf8name 的长度(以字节为单位),如果它以 null 结尾则为 NAPI_AUTO_LENGTH
  • constructor:类实例化时实际的构造函数,对C++侧来说,这是一个构造函数回调
  • data:作为回调信息的数据属性传递给构造函数回调的可选数据。
  • property_count:属性数组参数中的项目数
  • properties:属性描述符数组classDesc,描述类的静态和实例数据属性、访问器和方法
  • result:表示类的构造函数的 napi_value
对象创建和包装

JS侧无法直接使用C++侧的构造函数实例化对象,只有将C++侧的实例对象包装起来。new一个新的js对象时实际的构造函数MyObject::JSConstructor,在该函数内部实现如下:

对象创建

  • 通过napi_get_cb_info拿到构造函数调用时的js this对象(也叫做C++对象包装器),然后给this绑定实例属性

对象包装

  • 拿到从js侧收到的参数,创建C++类MyObject的实例对象obj
  • 通过napi_wrap,将js this对象和实例对象obj包装起来,即将js this指向实例对象obj,这类似于js new一个对象过程使用call或者apply改变this指向的操作。

所有,js侧调用构造函数MyObject::JSConstructor创建一个实例对象时,返回的是包装了C++类的实例对象的新对象

/**
 * new一个新的JS对象时实际的构造函数
 * @param env
 * @param info
 * @return 返回调用JS构造函数,new一个新对象时的this参数
 */
napi_value MyObject::JSConstructor(napi_env env, napi_callback_info info) {
  OH_LOG_INFO(LOG_APP, "=== MyObject::JSConstructor");
  
  // 获取函数的内部属性new.target
  napi_value new_target = nullptr;
  napi_get_new_target(env, info, &new_target);
  // 检测函数是否使用 new 关键字调用
  bool is_constructor = (new_target != nullptr);

  size_t argc = 2;
  napi_value argv[2] = {nullptr};
  napi_value _this = nullptr;

  // 获取构造函数入参
  napi_get_cb_info(env, info, &argc, argv, &_this, nullptr);

  // 设置JS对象的属性, _this.num1 = argv[0]...
  napi_set_named_property(env, _this, "num1", argv[0]);
  napi_set_named_property(env, _this, "num2", argv[1]);

  if (is_constructor) {
    double a, b;
    // 将从JS侧收到的参数格式转换为double
    napi_get_value_double(env, argv[0], &a);
    napi_get_value_double(env, argv[1], &b);

    // 创建类MyObject的实例
    MyObject *obj = new MyObject(a, b);
    obj->env_ = env;

    // 绑定JS对象和C++ Native对象
    napi_wrap(env, _this, obj, MyObject::Destructor, nullptr, &obj->wrapped_obj);

    // 返回绑定后的对象的this
    return _this;
  }
  return nullptr;
}

napi_get_new_target

napi_status napi_get_new_target(napi_env env,
                                napi_callback_info cbinfo,
                                napi_value* result)
// 获取函数的内部属性new.target
napi_value new_target = nullptr;
napi_get_new_target(env, info, &new_target);
// 检测函数是否使用 new 关键字调用
bool is_constructor = (new_target != nullptr);

此 API 返回构造函数调用的 new.target。如果当前回调不是构造函数调用,则结果为 nullptr

napi_wrap

当 JS调用构造函数时,构造函数回调使用 napi_wrap 将新的 C++ 实例对象包装在 JS对象中,然后返回JS对象。

napi_status napi_wrap(napi_env env,
                      napi_value js_object,
                      void* native_object,
                      napi_finalize finalize_cb,
                      void* finalize_hint,
                      napi_ref* result);
// 绑定JS对象和C++ Native对象
napi_wrap(env, _this, obj, MyObject::Destructor, nullptr, &obj->wrapped_obj);
  • js_object:将成为C++对象包装器的 js对象。该对象必须是从使用 napi_define_class() 创建的构造函数的原型创建的。
  • native_object:C++实例对象
  • finalize_cb:可用于在 js对象准备好进行垃圾回收时释放C++实例的回调函数。
对象解包装

在上面对象包装的过程中可知,JS侧拿到的实例对象是包装了C++实例对象的JS对象,而JS对象只声明并没有初始化实例对象的属性和方法。所以在调用实例对象上的方法MyObject::JSOperate时,需要先使用napi_unwrap将JS对象解包装,将C++实例对象取出来,然后再在实例对象拿到属性和方法,实现类的方法。

如下代码:

// JS调用类的方法
napi_value MyObject::JSOperate(napi_env env, napi_callback_info info) {
  OH_LOG_INFO(LOG_APP, "=== MyObject::JSOperate");
  napi_value argv[1] = {nullptr};
  size_t argc = 1;
  napi_value this_ = nullptr;
  
  napi_get_cb_info(env, info, &argc, argv, &this_, nullptr);

  MyObject *myObject;

  // 获取JS对象对应的C++ Native对象
  napi_unwrap(env, this_, reinterpret_cast<void **>(&myObject));

  // 将收到的JS参数保存在operatorType
  uint32_t operatorType = 0;
  napi_get_value_uint32(env, argv[0], &operatorType);

  double result = 0;
  if (operatorType == 0) {
    result = myObject->a + myObject->b;
  } else if (operatorType == 1) {
    result = myObject->a - myObject->b;
  } else if (operatorType == 2) {
    result = myObject->a * myObject->b;
  } else {
    if (myObject->b == 0) {
      result = 0;
    } else {
      result = myObject->a / myObject->b;
    }
  }
  // 将结果转成napi_value类型返回
  napi_value ans = nullptr;
  napi_create_double(env, result, &ans);

  return ans;
}

napi_unwrap

当 JS代码调用C++类上的方法或属性时,将调用相应的C++ 函数,napi_unwrap 将 C++ 实例对象从js对象中取出来。

napi_status napi_unwrap(napi_env env,
                        napi_value js_object,
                        void** result);
// 获取JS对象对应的C++ Native对象
napi_unwrap(env, this_, reinterpret_cast<void **>(&myObject));
  • js_object:C++对象包装器的 js对象
  • result:解包装后的的C++对象
对象回收

MyObject::JSConstructor中的对象包装napi_wrap中,指定了对象回收时的回调函数MyObject::Destructor,在该函数内就是释放被包装的C++实例对象

// 对象包装
napi_wrap(env, _this, obj, MyObject::Destructor, nullptr, &obj->wrapped_obj);

// 释放资源的函数(类似类的析构函数)
void MyObject::Destructor(napi_env env, void *nativeObject, void *finalize_hint) {
  OH_LOG_INFO(LOG_APP, "=== MyObject::Destructor");
  MyObject *obj = static_cast<MyObject *>(nativeObject);
  delete obj;
}

场景二:构造函数持久化,直接创建js类的实例对象

JS侧

在场景一的JS代码中添加一个button组件,点击 使用C++提供的方法直接获取一个实例对象。

Button('2. C++ directly create instance fo js')
    .margin({ top: 20 })
    .onClick(() => {
    this.myObject = testNapi.getNewInstance(5, 6);
    this.message = `myObject = new MyObject(${this.myObject.num1}, ${this.myObject.num2})`;
})

Native C++侧

napi_define_class 的调用结果为JS类实例化时的构造函数,可以持久化保存,方便以后直接创建js类的实例对象,或检查提供的值是否是类的实例。 在这种情况

下,为防止函数值被垃圾回收,使用 napi_create_reference 创建对它的持久引用并确保引用计数保持 >= 1。

// 根据C++提供的MyObject类,定义JS类,JS类的构造函数保存在constructor
napi_define_class(env, className, sizeof(className), MyObject::JSConstructor, nullptr, 1, classDesc, &constructor);

// 保存constructor,创建引用计数保持 >= 1(防止constructor_ref被垃圾回收),方便以后直接创建js类的实例对象
napi_create_reference(env, constructor, 1, &constructor_ref);

使用持久化保存的constructor_ref直接创建js类的实例对象:

// 创建一个类的实例,并返回到JS侧
napi_value GetNewInstance(napi_env env, napi_callback_info info) {
  OH_LOG_INFO(LOG_APP, "=== MyObject::CreateJSInstance");

  size_t argc = 2;
  napi_value argv[2] = {nullptr};

  // 获取构造函数入参
  napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);

  // 获取napi_define_class定义JS时保存的constructor_ref
  napi_value constructor = nullptr;
  napi_get_reference_value(env, MyObject::constructor_ref, &constructor);

  // 利于保存的js类构造函数直接创建实例对象
  napi_value instance;
  napi_new_instance(env, constructor, argc, argv, &instance);

  return instance;
}

napi_new_instance

napi_status napi_new_instance(napi_env env,
                              napi_value cons,
                              size_t argc,
                              napi_value* argv,
                              napi_value* result)
// 利于保存的js类构造函数直接创建实例对象
napi_new_instance(env, constructor, argc, argv, &instance); 
  • cons:之前保存的constructor_ref,实例化时作为JS构造函数被调用
  • argc和argv分别为参数个数和参数数组
  • result为创建的新实例对象

补充

JS使用构造函数new一个对象过程

  1. 在内存中创建一个新的空对象obj;
  2. 这个新对象内部的[[Prototype]]特性赋值为构造函数的 prototype 属性。也就是将新创建的空对象的__proto__指向其构造函数的prototype对象;
  3. 使用 apply/call 改变 this 的指向,将this指向执行新实例对象obj,并执行构造函数;
  4. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象obj

简言之:先创建一个新对象obj,给obj绑定构造函数的原型对象(方便继承原型属性和方法),然后将构造函数的this指向新对象obj(绑定实例属性和方法),最后返回创建的新对象。

new创建的新对象obj返回的结果其实是对象的引用this,类似于C++描述:Person *obj= new Person(xxx)。

测试一下new的过程:

function Person(identity) {
   this.identity = identity || 'Person';
}

// 模拟new过程
function _new(Fn) {
  return function() {
    const obj = {}; // 1
    obj.__proto__ = Fn.prototype; // 2
    const result = Fn.apply(obj, arguments); //3
    return typeof(result) === 'object' ? result : obj; // 4
  }
}
// 测试
var obj = _new(Person)('son');
console.log(obj.identity); // son
console.log(obj.constructor); // [Function: Person]

Native C++侧对象的创建和包装本质JS对象new一个对象过程

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值