大杰哥 發表於 2025-6-16 14:20:00

鸿蒙 Native API 的封装库 h2lib_arkbinder

<h1 id="h2lib_arkbinder">h2lib_arkbinder</h1>
<h2 id="介绍">介绍</h2>
<p>code: https://gitee.com/evanown/h2lib_arkbinder</p>
<p>本类库实现 C++ 代码到鸿蒙 Native API 的封装与转换。</p>
<p>现在鸿蒙生态还处于热火朝天的建设阶段,能否快速的将其他平台如IOS、Android的APP快速、高效、高质量的移植到鸿蒙系统,关系到鸿蒙的兴衰大业。在鸿蒙APP开发中,华为目前主推的是ArkTS语音,实际上就是Typescript的一种变体,除了UI等代码的迁移,很多APP的核心资产都是C++代码比如一些高效的图像处理算法等。鸿蒙提供了Native API相关接口,可以实现ArkTS调用C++代码。本类库arkbinder可以大幅度的提升鸿蒙Native API的易用性,如果你也移植APP的过程中要处理老的C++代码,那么本类库可能会极大的加速你的工作。</p>
<h2 id="目前native-api的问题">目前Native API的问题</h2>
<ol>
<li>Native Api的使用手册和示例代码都不算健全。APP的开发者对自己的C++资产代码是熟悉的,但是要通过ArkTS调用,则需要通过Native API封装,属性Native API的概念、api接口参数等细节,即枯燥乏味又常常让人疑惑,比如垃圾回收如何触发类的析构,文档实际给出了比较模糊的指引,比如 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/use-napi-object-wrap-V5 中使用垃圾回收是使用delete析构了对象,而 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/use-sendable-napi-V5 中示例只调用类的析构函数,但是没用使用delete析构内存,而在https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/use-napi-about-class-V5中DerefItem确不调用delete。</li>
<li>文档的示例代码似乎从不处理函数传功来的类型和数量,比如文档中最入门的导出c++的AddNum示例函数的代码如下,本人立即ArkTS是脚本代码,类型是允许传入任意类型的,而且鸿蒙的示例代码变量几乎不初始化(推断鸿蒙的核心开发都是搞C程序),如果运行期ArkTS传入的类型不合法(比如更新了版本?),那么这里是有安全隐患的。</li>
</ol>
<pre><code class="language-cpp">// 此模块是一个Node-API的回调函数
static napi_value Add(napi_env env, napi_callback_info info)
{
    // 接受传入两个参数
    size_t requireArgc = 2;
    size_t argc = 2;
    napi_value args = {nullptr};
    napi_get_cb_info(env, info, &amp;argc, args , nullptr, nullptr);//没有处理参数少于2个的情况

    // 将传入的napi_value类型的参数转化为double类型
    double valueLeft;
    double valueRight;
    napi_get_value_double(env, args, &amp;valueLeft);//!如果使用者不小心传入了字符串会怎样?
    napi_get_value_double(env, args, &amp;valueRight);
</code></pre>
<ol start="3">
<li>对于C++的多态、继承似乎也没有文档说明。这个可以理解,官方似乎还是希望你多用ArkTS,毕竟C++的概念太多很难跟ArkTS中的概念一一对应。但是如果C++的资产中存在类的继承已经多态函数(这很正常的对吧?),你肯定不想修改八百年不曾修改的老C++代码(导致其他平台不兼容?)。</li>
<li>如果自己编写Native API封装老的C++代码,对于开发者有很强的专业要求,既要对Typscript(实际上是javascript)的概念有了解,又要对NativeAPI的一些类型判断、引用计数等时刻谨慎,同时编写的代码还要高效安全,这无疑是费力难做好的工作,单似乎又要每个APP的开发者独立的做一遍。</li>
<li>本类库基于以上原因,开发了arkbinder,提供一个企业级、高效安全、教科书级的NativeAPI调用的封装。</li>
</ol>
<h2 id="arkbinder使用说明">arkbinder使用说明</h2>
<p>arkbinder移动三个宏H2_DEF_CLASS定义类,H2_DEF_ITERFACE定义函数、类方法、类属性,最后H2_DEF_MODULE_FINISH(模块名);完成注册。</p>
<h4 id="定义全局函数">定义全局函数</h4>
<p>如定义一个全局函数AddNum, 直接使用宏H2_DEF_ITERFACE注册,在所有注册完成后使用H2_DEF_MODULE_FINISH定义导出的模块名(必须和CMakeList中的so名一致),在ArkTS中就可以直接用了。</p>
<pre><code class="language-cpp">static double AddNum(double a, doubleb){
    return a*100 + b;
}
extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
    H2_DEF_ITERFACE(&amp;AddNum);
    H2_DEF_MODULE_FINISH("entry");//!一定要放在定义的最后
}
//ArkTS示例代码
    import cppExt from 'libentry.so';
    let a = cppExt.AddNum(2, 3);
</code></pre>
<h4 id="c类全局函数">C++类全局函数</h4>
<p>若要定义C++的静态函数,同样使用H2_DEF_ITERFACE宏,它会自动判断这个全局函数属于哪个类,并正确的注册到对应的类上</p>
<pre><code class="language-cpp">class Animal
{
public:
    static std::string PrintTypeName(){ return typeid(Animal).name();}
//注册代码如下
    H2_DEF_ITERFACE(&amp;Animal::PrintTypeName);//仍然使用宏注册
//ArkTS示例代码
    cppExt.Animal.PrintTypeName();
</code></pre>
<h4 id="注册c类">注册C++类</h4>
<p>定义构造函数使用H2_DEF_CLASS,支持多态,定义的时候需要显示的标明构造函数的类型</p>
<pre><code class="language-cpp">class Animal
{
public:
    Animal(std::string s=""):name(s),color(0){}
    Animal(std::string s, int c):name(s),color(c){}
//注册代码如下
    H2_DEF_CLASS(Animal(const std::string&amp;));
    H2_DEF_CLASS(Animal(const std::string&amp;, int));
//ArkTS示例代码
    let dog = new cppExt.Animal("dog");
    let dog2 = new cppExt.Animal("dog", 1);
</code></pre>
<h4 id="定义c类继承父类">定义C++类继承父类</h4>
<p>如果C++的类是继承于父类,并且父类已经使用H2_DEF_CLASS定义,那么子类定义是使用H2_DEF_CLASS2显示的标明父类。</p>
<pre><code class="language-cpp">classBird:public Animal{
public:
    Bird(std::string strName=""):Animal(strName){}
    virtualint Fly(int nHeight){
      return nHeight;
    }
//注册代码如下
    H2_DEF_CLASS2(Bird(std::string), Animal);
//ArkTS示例代码
    let eagle = new cppExt.Bird("eagle");
};
</code></pre>
<h4 id="c类属性">C++类属性</h4>
<p>H2_DEF_ITERFACE用来定义类中的属性字段,数值类型、字符串、已经注册过的类、指针等都支持。</p>
<pre><code class="language-cpp">struct Pos{
    Pos(int a = 0, int b = 0):x(a),y(b){}
    int x;
    int y;
};
//注册代码如下
    H2_DEF_CLASS(Pos(int, int));
    H2_DEF_ITERFACE(&amp;Pos::x);
    H2_DEF_ITERFACE(&amp;Pos::y);
//ArkTS示例代码
    let pos = new cppExt.Pos(50, 60);
</code></pre>
<h4 id="c类方法">C++类方法</h4>
<p>H2_DEF_ITERFACE一键定义类中的方法,支持所有标准数值、字符串、注册过的类,以及各种函数的const、引用的使用。你如果对C++模板编程感兴趣,不妨可以看看类库是如何用模板特化机制来实现对Native API的封装的。</p>
<pre><code class="language-cpp">class Animal
{
    virtualint Fly(int nHeight);
    void SetColor(int c);
    int GetColor() const;
    const std::string&amp; GetName() const;
    void SetName(const char* s);
    const Pos&amp; GetPos() const;
//注册代码如下
    H2_DEF_ITERFACE(&amp;Animal::GetColor);
    H2_DEF_ITERFACE(&amp;Animal::SetColor);
    H2_DEF_ITERFACE(&amp;Animal::SetName);
    H2_DEF_ITERFACE(&amp;Animal::GetName);
    H2_DEF_ITERFACE(&amp;Animal::GetPos);
//ArkTS示例代码
    dog.SetColor(66);
    let c = dog.GetColor();
    let n = dog2.GetName();
</code></pre>
<h4 id="c类多态">C++类多态</h4>
<p>示例代码中Go函数是多态的,注册的时候要显示的标明注册的函数类型,其实就是转成函数指针,这样编译器就知道你要注册哪个了。</p>
<pre><code class="language-cpp">class Animal
{
    virtual void Go(int x, int y);
    virtual void Go(const Pos&amp; p);
//注册代码如下
    H2_DEF_ITERFACE((void(Animal::*)(int, int))(&amp;Animal::Go));
    H2_DEF_ITERFACE((void(Animal::*)(const Pos&amp;))&amp;Animal::Go);
//ArkTS示例代码
    dog.Go(100, 200);
    let pos = new cppExt.Pos(50, 60);
    dog.Go(pos);
</code></pre>
<h4 id="嵌套支持stl容器">嵌套支持STL容器</h4>
<p>arkbinder通过泛型模版编程嵌套的支持vector、list、set、map,所有这四个类型的任意组合和嵌套,全部支持。<br>
容器的元素类型支持标准数值、字符串、string、注册的类对象、指针以及嵌套的这四个容器类型。</p>
<pre><code class="language-cpp">class Animal
{
    std::map&lt;std::string, std::string&gt; Dump(){
      std::map&lt;std::string, std::string&gt; ret;
      ret["name"] = name;
      ret["color"] = std::to_string(color);
      ret["pos"] = "["+std::to_string(pos.x)+","+std::to_string(pos.y)+"]";
      return ret;
    }
//注册代码如下
    H2_DEF_ITERFACE(&amp;Animal::Dump);
//ArkTS示例代码
    let m = dog.Dump();
    let ms = JSON.stringify(m, null, 4);
    hilog.info(DOMAIN, 'testTag', 'stl = %{public}s', ms);
</code></pre>
<h4 id="支持将c类对象引用值对象传入arkts">支持将C++类对象、引用、值对象传入ArkTS</h4>
<p>已经定义的C++类指针和引用以及const引用都可以传入ArkTS,实际上以临时类指针的方式传入ArkTs,这种情况,ArkTS是不负责垃圾回收的,对象的生命周期管理仍然由C++负责,如果函数参数或者函数返回值为类对象的值对象,那么实际上arkbinder内部会new一个对象给arkTS,这样的临时对象是由artTS的垃圾回收负责的。</p>
<pre><code class="language-cpp">class Animal
{
    virtual const Animal* GetSelf() {
      return this;
    }
    bool IsSame(const Animal&amp; p) const{
      return &amp;p == this;
    }
    Pos AllocPos();
//注册代码如下
    H2_DEF_ITERFACE(&amp;Animal::GetSelf);
    H2_DEF_ITERFACE(&amp;Animal::IsSame);//上面两个接口指针都是临时的,arkTS垃圾回收不会触发对象析构
    H2_DEF_ITERFACE(&amp;Animal::AllocPos);//!这里返回的Pos对象垃圾回收是ArkTs负责的。
//ArkTS示例代码
    let dog2 = dog.GetSelf();
    dog.IsSame(dog2)
</code></pre>
<h4 id="支持将stdfunction和arkts的function进行互相映射">支持将std::function和ArkTS的function进行互相映射</h4>
<p>如果将ArkTs的function作为函数参数,自动转换成C++的std::function。<br>
如果将std::function转入到ArkTS中,std::function会转换成ArkTS的一个类对象CppLambdaFunc,调用<br>
call回调c++的函数,参数为任意参数类型的一个列表。</p>
<pre><code class="language-TypeScript">export class CppLambdaFunc{
call: (a: Array&lt;any|number&gt;) =&gt; string;
}
</code></pre>
<pre><code class="language-cpp">class Animal
{
    int Touch(std::function&lt;int(double)&gt; cb){
      return cb(66.99)+100;
    }
    std::function&lt;std::string(int)&gt; GenFunc(){
      return [](int v)-&gt;std::string{
            std::string ret = "GenFunc:"+std::to_string(v);
            return ret;
      };
    }
//注册代码如下   
    H2_DEF_ITERFACE(&amp;Animal::Touch);
    H2_DEF_ITERFACE(&amp;Animal::GenFunc);
//ArkTS示例代码
    let tret = dog.Touch((a:number)=&gt;{
      return a+2000;
    });
    let cppcb = dog.GenFunc();
    let cppcb_ret = cppcb.call();
</code></pre>
<h2 id="附加说明">附加说明</h2>
<h4 id="高度可扩展">高度可扩展</h4>
<p>如果你使用了其他类型,在arkbinder中没有支持导致编译不过,自己可以通过模板特化实现扩展,因为arkbinder只有一个头文件完全模板代码,所以可以在编译器扩展。支持新类型只需要对ScriptCppTypeTraits类进行特化,编写两个函数,static void Script2CppType(napi_env env, napi_value nv, T&amp; ret)函数将ArkTS中的类型转换为C++的类型,而static void Cpp2ScriptType(napi_env env, napi_value&amp; ret, const T&amp; val){则相反,将c++的类型转换成ArkTS类型。如std::string 的特化代码如下</p>
<pre><code class="language-cpp">
template&lt;&gt;
struct ScriptCppTypeTraits&lt;std::string&gt;
{
        static int TypeVal() { return napi_string;}
    static void Script2CppType(napi_env env, napi_value nv, std::string&amp; ret){
      if (!nv){
            return;
      }
      napi_valuetype valuetype0;
      napi_typeof(env, nv, &amp;valuetype0);
      switch (valuetype0){
            case napi_number:{
                double tmpv = 0.0;
                napi_get_value_double(env, nv, &amp;tmpv);
                ret = std::to_string(tmpv);
            }break;
            case napi_bigint:{
                int64_t tmpv = 0;
                napi_get_value_int64(env, nv, &amp;tmpv);
                ret = std::to_string(tmpv);
            }break;
            case napi_string:{
                size_t length = 0;
                napi_status status = napi_get_value_string_utf8(env, nv, nullptr, 0, &amp;length);
                // 传入一个非字符串 napi_get_value_string_utf8接口会返回napi_string_expected
                if (status != napi_ok) {
                  return;
                }
                ret.reserve(length+1);
                ret.resize(length, 0);
                (void)ret.c_str();
                napi_get_value_string_utf8(env, nv, &amp;ret, length + 1, &amp;length);
            }break;
            default:{
            }break;
      }
    }
    static void Cpp2ScriptType(napi_env env, napi_value&amp; ret, const std::string&amp; val){
      napi_create_string_utf8(env, val.c_str(), val.size(), &amp;ret);
    }
    static void Cpp2ScriptType(napi_env env, napi_value&amp; ret, const char* val){
      napi_create_string_utf8(env, val, NAPI_AUTO_LENGTH, &amp;ret);
    }
    template&lt;typename R&gt;
    static void Cpp2ScriptType(napi_env env, napi_value&amp; ret, R val){
      std::ostringstream oss;
      oss &lt;&lt; val;
      std::string strVal = oss.str();
      napi_create_string_utf8(env, strVal.c_str(), strVal.size(), &amp;ret);
    }
};
</code></pre>
<h4 id="导出后arkts声明文件">导出后ArkTS声明文件</h4>
<p>ArkTS是有类型的语音,C++中的接口需要显示的声明才能在ArkTS中编译通过,这一步目前还是需要手动编写。<br>
TODO:arkbinder可以默认有一个DumpInterface接口将注册的所有类和接口自动导出声明文件,由于时间原因暂时没有时间编写,哪位江湖侠客敢兴趣可自行编写一个。</p>
<pre><code class="language-TypeScript">export const AddNum: (a: number, b: number) =&gt; number;
export class Pos {
constructor(x?: number, y?:number);

x:number;
y:number;
}
export class CppLambdaFunc{
call: (a: Array&lt;any|number&gt;) =&gt; string;
}
export class Animal {
constructor(name: string, c?:number);
static PrintTypeName():string;
Go: (a: number|Pos, b?:number) =&gt; void;
GetColor: () =&gt; number;
SetColor: (a: number) =&gt; void;
SetName: (a: string) =&gt; void;
GetName: () =&gt; string;
GetSelf:()=&gt;Animal;
IsSame:(a:Animal)=&gt;boolean;
Fly: (a: number) =&gt; number;

Dump:(a:number[])=&gt;object;
GetPos:()=&gt;Pos;
Touch:(cb:any)=&gt;number;
GenFunc:()=&gt;CppLambdaFunc;


}
export class Bird extends Animal{
constructor(arg: string);

}
</code></pre>
<h4 id="以上">以上。</h4><br><br>
来源:https://www.cnblogs.com/zhiranok/p/18931086/arkbinder
頁: [1]
查看完整版本: 鸿蒙 Native API 的封装库 h2lib_arkbinder