前言
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++类实例对象包装和导出的具体过程:
场景一:对象包装,返回给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一个对象过程:
- 在内存中创建一个新的空对象
obj
; - 这个新对象内部的
[[Prototype]]
特性赋值为构造函数的prototype
属性。也就是将新创建的空对象的__proto__
指向其构造函数的prototype
对象; - 使用
apply
/call
改变this
的指向,将this
指向执行新实例对象obj
,并执行构造函数; - 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
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一个对象过程