Auto.js Pro 9调用C/C++插件 (Addon)


作者: 神麤詭末

在auto.js更新nodejs引擎之前,想调用c/c++可以使用动态加载jni的方法,而现在则可以使用nodejs本身的addon插件功能去调用c/c++,极大的拓展了使用aj编程的上限。


先贴一个nodejs官方的addon说明: https://nodejs.org/dist/latest-v16.x/docs/api/addons.html

然后是本教程需要用到的东西:

  1. aide与可用的aide-ndk,可在autojs-nodejs-addon下载。
  2. auto.js pro9的libnode.so,解压安装包可得。
  3. auto.js pro9中对应版本的nodejs的一系列头文件,可在node源码存储库下载。
  4. nan,node-addon-api等addon api扩展库头文件,可在aj终端npm i直接下载。

如何获取与使用后文会一步步教学,本教程用到的所有项目文件都可以在autojs-nodejs-addon下载。

本人QQ: 2682963017 欢迎大佬们提示纠错。

特别感谢aj作者的提示点拨,以及内测群里恒道无惑和我一起不断研究试错。

ps: aj yyds!(划掉)

aj nodejs调用c/c++的三种方法

JNI: 动态加载so与对应的dex,在aj的nodejs环境中套娃过多,调用性能最差。JNI

WASM: js新标准之一,可将c/c++编译为wasm再调用,api限制似乎不少,调用性能最高。WASM

ADDON: nodejs自带的c/c++插件功能,可直接require导入.node文件调用api,原生nodejs的插件编译十分简单,但aj环境下的编译比较偏门,调用性能高于jni。ADDON

ps1: 调用性能不等于运算性能,js自身的函数调用性能当然是碾压三者的。JS
ps2: 其实也可以写个安卓原生app,接入aj的plugins系统,然后在aj利用plugins模块去调用app的jni,运算性能不好说,调用性能更不好说。。。

目录

一 在手机上利用aide(重点)为aj编译addon插件

  1. 下载aide,创建项目,安装aide-ndk
  2. 解包aj复制so
  3. 获取addon需要的头文件
  4. 编译hello.cpp

二 在auto.js pro9导入addon,使用addon的拓展api

  1. 在aj导入二进制文件
  2. 在aj导入本地addon文件
  3. 使用nan
  4. 使用node-addon-api

结语


在手机上利用aide(重点)为aj编译addon插件

使用手机编译的原因:

  1. 操作便捷,不需要进行复杂的环境配置。
  2. 符合autojs手机编程的宗旨。
  3. 无需频繁传输文件,方便测试
  4. 目前网上欠缺手机编译的先例,我希望在此做出探索。
人话

为什么要在手机上编译呢?主要是本人喜欢在手机上操作,毕竟aj就是手机的,用电脑每次都要传文件,环境配置也烦人。
而且电脑上编译的教程很多,编译安卓可用的so也很好搞定,不需要我再写一个教程了。
手机上的教程则是基本找不到,尤其是本教程这种偏门的需求。

使用aide来编译,而不是在termux里面安装AndroidStudio for Linux之类的方法,主要是aide操作更方便,对存储空间的占用也更小。
当然功能也更弱一些就是了。


下载aide,创建项目,安装aide-ndk

  1. 可在前文的阿里云链接中下载,也可以自己准备,能用就行。

0
1

  1. 打开aide,直接创建个原生项目,应用名与包名随便写,不报错就行。

2
3
4

  1. 取消aide的ndk下载弹窗,点右上角的三竖点>更多>设置,选择第四个构建 & 运行

构建 & 运行

  1. 选择第五个管理Native Code,填写之前准备的aide-ndk压缩包绝对路径,点击安装后等待安装完成。

管理Native Code
填写路径
等待安装
安装完成


解包aj复制so

  1. 解压aj安装包,直接把lib文件夹复制到aide项目的jni文件夹里,并改名为libs。

复制lib

aj是双架构的,本教程也将直接编译双架构的addon,所以就不需要把so从架构文件夹取出来了。
双架构支持

  1. 把架构文件夹中多余的so删除,只留下libnode.so。

    不删除也没有影响

arm64-v8a
armeabi-v7a


准备addon需要的头文件

  1. 在aj的nodejs环境中使用

    console.log(`Node.js版本: ${process.version}`);

    获取aj的nodejs版本。
    Node.js版本

  2. 在前文的node存储库中,选择16x的分支然后下载源码。

选择分支
下载源码

  1. 在aide项目的jni文件夹中创建includes文件夹,再于includes创建nodejs、v8文件夹。

nodejs与v8

  1. 解压下载的nodejs源码,将node-16.x/src中的内容复制到前面创建的includes/nodejs里,将node-16.x/deps/v8/include的内容复制到includes/v8。

includes/nodejs
includes/v8

应该可以只留.h文件,但我懒得删了


配置aide的编译项

  1. jni/APPLICATION.MK中,NDK_TOOLCHAIN_VERSION改为clang,APP_STL改为c++_shared,APP_ABI的x86改成arm64-v8a。

APPLICATION.MK

  1. jni/ANDROID.MK中,LOCAL_PATH后面添加代码
    include $(CLEAR_VARS)
    LOCAL_MODULE_TAGS       := optional
    LOCAL_MODULE            := libnode
    LOCAL_SRC_FILES         := libs/${TARGET_ARCH_ABI}/libnode.so
    include $(PREBUILT_SHARED_LIBRARY)
    在LOCAL_SRC_FILES := hello-jni.cpp后添加代码
    LOCAL_LDFLAGS   := $(LOCAL_PATH)/libs/${TARGET_ARCH_ABI}/libnode.so
    LOCAL_C_INCLUDES += $(LOCAL_PATH)/includes/nodejs $(LOCAL_PATH)/includes/v8
    并修改hello-jni为hello。
    ANDROID.MK

编译hello.cpp

  1. 复制node官方文档中的hello.cc代码
    #include <node.h>
    
    namespace demo {
    
    using v8::FunctionCallbackInfo;
    using v8::Isolate;
    using v8::Local;
    using v8::Object;
    using v8::String;
    using v8::Value;
    
    void Method(const FunctionCallbackInfo<Value>& args) {
      Isolate* isolate = args.GetIsolate();
      args.GetReturnValue().Set(String::NewFromUtf8(
          isolate, "world").ToLocalChecked());
    }
    
    void Initialize(Local<Object> exports) {
      NODE_SET_METHOD(exports, "hello", Method);
    }
    
    NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
    
    }  // namespace demo
    到jni/hello.cpp中
    hello.cpp
  2. 直接打包会有好几个报错,首先是

std报错
解决办法,在ANDROID.MK中加上一行

LOCAL_CPPFLAGS  += -std=c++17

std=c++17
然后是
未设置APP_PLATFORM
其实现在已经可以编译成功了,但默认的android-14着实有点低,我用的aide-ndk最高支持android-28,所以还是在APPLICATION.MK里面加上一行

APP_PLATFORM := android-28

APP_PLATFORM := android-28

  1. 现在再打包已经没有问题了,正常的执行到了自动安装这一步。

打包

开始打包apk就代表编译完成了

跳出安装
项目中 的libs文件夹内也有了arm64-v8a与armeabi-v7a的子文件夹,分别都存有三个so文件,其中libhello.so就是我们需要的addon。

ps: 不是jni里面那个libs文件夹。

编译成功的libs
编译成功的arm64-v8a
编译成功的armeabi-v7a


在auto.js pro9导入addon,使用addon的拓展api

前文里,我们成功的利用aide的原生app打包功能,将官方示例中的c++代码编译成为了二进制文件,但这只是代表着经过了编译这一关,现在我们要尝试在aj内导入并使用它。

并且会继续尝试使用nan、node-addon-api这俩个对addon api的封装库,测试http://nodejs.github.io/node-addon-examples/中的部分addon示例的运行效果。


在aj导入二进制文件

  1. 创建nodejs项目。

wasm-time

  1. 将之前aide项目中libs文件夹复制到nodejs项目中,注意不是jni文件夹中的那个libs,并将libhello.so改名为hello.node。

    重点是后缀 .node

复制编译后的libs
hello.node arm64-v8a
hello.node armeabi-v7a

  1. 像官方文档里面一样直接
var $arch = ({
    arm: "armeabi-v7a",
    arm64: "arm64-v8a"
})[process.arch];
var addon = require(`./libs/${$arch}/hello.node`);

然后不出意外的报错了
require报错
很明显,nodejs对于安卓本地存储中的addon没有调用权限,万幸的是aj提供将app内部目录作为脚本文件夹的功能。

  1. 修改脚本文件为aj内部目录,然后复刻之前在本地存储的操作

    怎么把之前的本地文件弄到新目录里面不用说了吧,在aj终端一个cp -r就行。

项目复刻
main复刻
内部目录下的addon终于可以被nodejs正常导入了
测试导入
测试api也没有问题

console.log(addon, addon.hello());

hello测试

大功告成!

ps: 并没有,以上步骤导入的addon会有一个,无法在aj运行期间多次导入的问题,必须是清后台重进aj后的第一次运行js导入才正常,之后再运行js都会导入报错,似乎不少人都遇到过(在正常的nodejs环境中),但在后文中此bug会消失。


在aj导入本地addon文件

  1. 利用process.env.AUTOJS_NATIVE_LIBRARY_PATH封装一个requireAddon,AUTOJS_NATIVE_LIBRARY_PATH指向的是aj的某个内部目录。
    function requireAddon(add_path) {
        var path = require("path");
        var fs = require("fs")
        var sourceBinPath = path.resolve(__dirname, add_path);
        const targetBinDir = path.join(process.env.AUTOJS_NATIVE_LIBRARY_PATH, '_addon');
        const targetBinPath = path.join(targetBinDir, path.basename(sourceBinPath).split(".")[0] + ".node");
        if (fs.existsSync(targetBinDir)) {
            fs.existsSync(targetBinPath) && fs.unlinkSync(targetBinPath);
        } else {
            fs.mkdirSync(targetBinDir, {
                recursive: true
            })
        }
        fs.copyFileSync(sourceBinPath, targetBinPath)
        return require(targetBinPath)
    }
  2. 换回之前本地的nodejs项目,复制粘贴上面封装的requireAddon,修改js内容为
    //获取本机架构映射
    var $arch = ({
        arm: "armeabi-v7a",
        arm64: "arm64-v8a"
    })[process.arch];
    var addon = requireAddon(`./libs/${$arch}/hello.node`);
    console.log(addon, addon.hello())
    
    function requireAddon(add_path) {
        var path = require("path");
        var fs = require("fs")
        var sourceBinPath = path.resolve(__dirname, add_path);
        const targetBinDir = path.join(process.env.AUTOJS_NATIVE_LIBRARY_PATH, '_addon');
        const targetBinPath = path.join(targetBinDir, path.basename(sourceBinPath).split(".")[0] + ".node");
        if (fs.existsSync(targetBinDir)) {
            fs.existsSync(targetBinPath) && fs.unlinkSync(targetBinPath);
        } else {
            fs.mkdirSync(targetBinDir, {
                recursive: true
            })
        }
        fs.copyFileSync(sourceBinPath, targetBinPath)
        return require(targetBinPath)
    }
    直接运行没有问题,且之前不能多次导入的问题也没有了。
    本地调用

    所以说之前的问题没了,是因为存放的路径,还是因为现在每次其实都是导入的不同的复制文件?

现在的aj打包时可选择禁用nodejs引擎,默认的是自动,建议改成启用防止nodejs未打包到apk。


小测试(小重点)

我们修改requireAddon函数的内容为

function requireAddon(add_path, force = true) {
    var path = require("path");
    var fs = require("fs")
    var sourceBinPath = path.resolve(__dirname, add_path);
    var isStorage = sourceBinPath.startsWith("/storage") || sourceBinPath.startsWith("/sdcard");
    const targetBinDir = path.join(process.env.AUTOJS_NATIVE_LIBRARY_PATH, '_addon');
    const targetBinPath = path.join(targetBinDir, path.basename(sourceBinPath).split(".")[0] + ".node");
    fs.cpSync(sourceBinPath, targetBinPath, {
        force,
    });
    return require(targetBinPath);
}

使用

var addon = requireAddon(`./libs/${$arch}/hello.node`);
console.log(addon, addon.hello())

force = true

再次修改代码

>var addon = requireAddon(`./libs/${$arch}/hello.node`, false);
console.log(addon, addon.hello())

force = false

看来每个addon文件仍旧只能导入一次,但我们的requireAddon用”巧妙”的办法绕过了这个问题,可能是编译配置仍有不全的地方?


使用nan

  1. 在终端直接npm i nan,然后把node_modules/nan复制到aide项目的jni/includes中

npm i nan
复制nan
还有nodejs源码中node-16.x/deps/uv/include里面的内容,也复制到jni/includes/uv里
复制uv

  1. 在ANDROID.MK的头文件导入中加上nan与uv

    LOCAL_C_INCLUDES += $(LOCAL_PATH)/includes/nodejs\
                        $(LOCAL_PATH)/includes/v8\
                        $(LOCAL_PATH)/includes/nan\
                        $(LOCAL_PATH)/includes/uv

    或者直接传入所有includes文件夹中的子文件夹

    LOCAL_C_INCLUDES += $(wildcard $(LOCAL_PATH)/includes/*)

    不会有人不用批量导入,真的打算一个个写吧?

  2. 在前文的示例存储库中,选择2_function_arguments示例中的nan文件夹下的addon.cc,复制到aide项目的jni文件夹中。

复制addon.cc

  1. 再次修改ANDROID.MK的内容,然后编译

    LOCAL_PATH := $(call my-dir)
    
    #根据架构导入libs文件夹中对应的libnode.so
    include $(CLEAR_VARS)
    LOCAL_MODULE_TAGS       := optional
    LOCAL_MODULE            := libnode
    LOCAL_SRC_FILES         := libs/${TARGET_ARCH_ABI}/libnode.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_CPP_EXTENSION := .cpp .cc
    LOCAL_MODULE    := hello
    LOCAL_SRC_FILES := hello.cpp
    #让std支持到c++17的标准
    LOCAL_CPPFLAGS  += -std=c++17
    #链接libnode.so
    LOCAL_LDFLAGS   := $(LOCAL_PATH)/libs/${TARGET_ARCH_ABI}/libnode.so
    #导入所需要的头文件
    LOCAL_C_INCLUDES += $(wildcard $(LOCAL_PATH)/includes/*)
    ifeq ($(TARGET_ARCH_ABI),x86)
        LOCAL_CFLAGS += -ffast-math -mtune=atom -mssse3 -mfpmath=sse
    endif
    include $(BUILD_SHARED_LIBRARY)
    
    #同时编译出hello与addon两个二进制文件。
    include $(CLEAR_VARS)
    LOCAL_CPP_EXTENSION := .cpp .cc
    LOCAL_MODULE    := addon
    LOCAL_SRC_FILES := addon.cc
    #让std支持到c++17的标准
    LOCAL_CPPFLAGS  += -std=c++17
    #链接libnode.so
    LOCAL_LDFLAGS   := $(LOCAL_PATH)/libs/${TARGET_ARCH_ABI}/libnode.so
    #导入所需要的头文件
    LOCAL_C_INCLUDES += $(wildcard $(LOCAL_PATH)/includes/*)
    ifeq ($(TARGET_ARCH_ABI),x86)
        LOCAL_CFLAGS += -ffast-math -mtune=atom -mssse3 -mfpmath=sse
    endif
    include $(BUILD_SHARED_LIBRARY)

    看到app开始打包就代表编译成功了
    app开始打包
    libs的架构文件夹里面也出现了libaddon.so
    编译成果

  2. 修改nodejs项目的代码,这次直接从aide项目中的libs文件夹里导入二进制文件,反正requireAddon的封装让我们可以这么做

//获取本机架构映射
var $arch = ({
    arm: "armeabi-v7a",
    arm64: "arm64-v8a"
})[process.arch];
//同时导入两个addon
var hello = requireAddon(`/storage/emulated/0/AppProjects/AutojsNodejsAddon/libs/${$arch}/libhello.so`);
var function_arguments = requireAddon(`/storage/emulated/0/AppProjects/AutojsNodejsAddon/libs/${$arch}/libaddon.so`);

console.log(hello, function_arguments);
console.log(hello.hello(), function_arguments.add(5, 29));

function requireAddon(add_path, force = true) {
    var path = require("path");
    var fs = require("fs")
    var sourceBinPath = path.resolve(__dirname, add_path);
    var isStorage = sourceBinPath.startsWith("/storage") || sourceBinPath.startsWith("/sdcard");
    const targetBinDir = path.join(process.env.AUTOJS_NATIVE_LIBRARY_PATH, '_addon');
    const targetBinPath = path.join(targetBinDir, path.basename(sourceBinPath).split(".")[0] + ".node");
    fs.cpSync(sourceBinPath, targetBinPath, {
        force,
    });
    return require(targetBinPath);
}

运行成功,没有问题
双addon导入运行


使用node-addon-api

  1. 与nan一样的npm i node-addon-api下载后复制到aide项目的jni/includes里面

npm i node-addon-api
复制node-addon-api

因为写了头文件批量导入,所以不用再手动添加node-addon-api的头文件导入了

  1. 在前文的示例存储库中,选择typed_threadsafe_function示例,里面只有一个node-addon-api版本,把clock.cc复制到aide项目的jni文件夹下

复制clock.cc
在ANDROID.MK里面添加

#再加一个clock的二进制编译
include $(CLEAR_VARS)
LOCAL_CPP_EXTENSION := .cpp .cc
LOCAL_MODULE    := clock
LOCAL_SRC_FILES := clock.cc
#让std支持到c++17的标准
LOCAL_CPPFLAGS  += -std=c++17
#链接libnode.so
LOCAL_LDFLAGS   := $(LOCAL_PATH)/libs/${TARGET_ARCH_ABI}/libnode.so
#导入所需要的头文件
LOCAL_C_INCLUDES += $(wildcard $(LOCAL_PATH)/includes/*)
ifeq ($(TARGET_ARCH_ABI),x86)
    LOCAL_CFLAGS += -ffast-math -mtune=atom -mssse3 -mfpmath=sse
endif
include $(BUILD_SHARED_LIBRARY)
  1. 开始编译,结果报错了

clock.cc报错
很明显,clock.cc与node-addon-api都需要我们开启错误捕获,修改APPLICATION.MK

NDK_TOOLCHAIN_VERSION := clang
APP_STL := c++_shared
APP_ABI := armeabi-v7a arm64-v8a
APP_PLATFORM := android-28
#node-addon-api必须开启异常捕获或者添加宏NAPI_DISABLE_CPP_EXCEPTIONS
APP_CPPFLAGS += -fexceptions

再次开始编译,成功。
clock编译成功

  1. 在typed_threadsafe_function示例的js中查看出clock的用法,修改我们的nodejs项目代码然后运行
//获取本机架构映射
var $arch = ({
    arm: "armeabi-v7a",
    arm64: "arm64-v8a"
})[process.arch];
//同时导入三个addon
var hello = requireAddon(`/storage/emulated/0/AppProjects/AutojsNodejsAddon/libs/${$arch}/libhello.so`);
var function_arguments = requireAddon(`/storage/emulated/0/AppProjects/AutojsNodejsAddon/libs/${$arch}/libaddon.so`);
var typed_threadsafe_function = requireAddon(`/storage/emulated/0/AppProjects/AutojsNodejsAddon/libs/${$arch}/libclock.so`);

console.log(hello, function_arguments);
console.log(hello.hello(), function_arguments.add(5, 29));

typed_threadsafe_function.start.call(new Date(), function (clock) {
    const context = this;
    console.log(context, clock);
}, 5);


function requireAddon(add_path, force = true) {
    var path = require("path");
    var fs = require("fs")
    var sourceBinPath = path.resolve(__dirname, add_path);
    var isStorage = sourceBinPath.startsWith("/storage") || sourceBinPath.startsWith("/sdcard");
    const targetBinDir = path.join(process.env.AUTOJS_NATIVE_LIBRARY_PATH, '_addon');
    const targetBinPath = path.join(targetBinDir, path.basename(sourceBinPath).split(".")[0] + ".node");
    fs.cpSync(sourceBinPath, targetBinPath, {
        force,
    });
    return require(targetBinPath);
}

运行成功!
运行成功


结语

说实话,本人的技术很低,基础也很差,对此教程内容的研究全是兴趣使然,一知半解,与恒道无惑大佬一块研究好几天后,侥幸的我先成功了。(输入法剪切板500条全是与此相关的东西)

大佬觉得这是使用aj编程的一次技术突破,有什么什么样的应用场景,但对我来说,只是因为想在aj调用nodejs的addon,仅此而已,搞定了,写个教程纪念一下,就丢一边去了,很少会有真正用上的时候。

如有大佬发现了本教程中的错漏之处,请加QQ告诉我,毕竟自己的问题,不能误导了别人。

完。

与君共勉之。


本文不允许转载。
  目录