作者: 神麤詭末
在auto.js更新nodejs引擎之前,想调用c/c++可以使用动态加载jni的方法,而现在则可以使用nodejs本身的addon插件功能去调用c/c++,极大的拓展了使用aj编程的上限。
先贴一个nodejs官方的addon说明: https://nodejs.org/dist/latest-v16.x/docs/api/addons.html
然后是本教程需要用到的东西:
- aide与可用的aide-ndk,可在autojs-nodejs-addon下载。
- auto.js pro9的libnode.so,解压安装包可得。
- auto.js pro9中对应版本的nodejs的一系列头文件,可在node源码存储库下载。
- 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环境中套娃过多,调用性能最差。
WASM: js新标准之一,可将c/c++编译为wasm再调用,api限制似乎不少,调用性能最高。
ADDON: nodejs自带的c/c++插件功能,可直接require导入.node文件调用api,原生nodejs的插件编译十分简单,但aj环境下的编译比较偏门,调用性能高于jni。
ps1: 调用性能不等于运算性能,js自身的函数调用性能当然是碾压三者的。
ps2: 其实也可以写个安卓原生app,接入aj的plugins系统,然后在aj利用plugins模块去调用app的jni,运算性能不好说,调用性能更不好说。。。
在手机上利用aide(重点)为aj编译addon插件
使用手机编译的原因:
- 操作便捷,不需要进行复杂的环境配置。
- 符合autojs手机编程的宗旨。
- 无需频繁传输文件,方便测试
- 目前网上欠缺手机编译的先例,我希望在此做出探索。
人话
为什么要在手机上编译呢?主要是本人喜欢在手机上操作,毕竟aj就是手机的,用电脑每次都要传文件,环境配置也烦人。
而且电脑上编译的教程很多,编译安卓可用的so也很好搞定,不需要我再写一个教程了。
手机上的教程则是基本找不到,尤其是本教程这种偏门的需求。
使用aide来编译,而不是在termux里面安装AndroidStudio for Linux之类的方法,主要是aide操作更方便,对存储空间的占用也更小。
当然功能也更弱一些就是了。
下载aide,创建项目,安装aide-ndk
- 可在前文的阿里云链接中下载,也可以自己准备,能用就行。
- 打开aide,直接创建个原生项目,应用名与包名随便写,不报错就行。
- 取消aide的ndk下载弹窗,点右上角的三竖点>更多>设置,选择第四个构建 & 运行。
- 选择第五个管理Native Code,填写之前准备的aide-ndk压缩包绝对路径,点击安装后等待安装完成。
解包aj复制so
- 解压aj安装包,直接把lib文件夹复制到aide项目的jni文件夹里,并改名为libs。
aj是双架构的,本教程也将直接编译双架构的addon,所以就不需要把so从架构文件夹取出来了。
- 把架构文件夹中多余的so删除,只留下libnode.so。
不删除也没有影响
准备addon需要的头文件
在aj的nodejs环境中使用
console.log(`Node.js版本: ${process.version}`);
获取aj的nodejs版本。
在前文的node存储库中,选择16x的分支然后下载源码。
- 在aide项目的jni文件夹中创建includes文件夹,再于includes创建nodejs、v8文件夹。
- 解压下载的nodejs源码,将node-16.x/src中的内容复制到前面创建的includes/nodejs里,将node-16.x/deps/v8/include的内容复制到includes/v8。
应该可以只留.h文件,但我懒得删了
配置aide的编译项
- jni/APPLICATION.MK中,NDK_TOOLCHAIN_VERSION改为clang,APP_STL改为c++_shared,APP_ABI的x86改成arm64-v8a。
- jni/ANDROID.MK中,LOCAL_PATH后面添加代码
在LOCAL_SRC_FILES := hello-jni.cpp后添加代码include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := libnode LOCAL_SRC_FILES := libs/${TARGET_ARCH_ABI}/libnode.so include $(PREBUILT_SHARED_LIBRARY)
并修改hello-jni为hello。LOCAL_LDFLAGS := $(LOCAL_PATH)/libs/${TARGET_ARCH_ABI}/libnode.so LOCAL_C_INCLUDES += $(LOCAL_PATH)/includes/nodejs $(LOCAL_PATH)/includes/v8
编译hello.cpp
- 复制node官方文档中的hello.cc代码
到jni/hello.cpp中#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
- 直接打包会有好几个报错,首先是
解决办法,在ANDROID.MK中加上一行
LOCAL_CPPFLAGS += -std=c++17
然后是
其实现在已经可以编译成功了,但默认的android-14着实有点低,我用的aide-ndk最高支持android-28,所以还是在APPLICATION.MK里面加上一行
APP_PLATFORM := android-28
- 现在再打包已经没有问题了,正常的执行到了自动安装这一步。
开始打包apk就代表编译完成了
项目中 的libs文件夹内也有了arm64-v8a与armeabi-v7a的子文件夹,分别都存有三个so文件,其中libhello.so就是我们需要的addon。
ps: 不是jni里面那个libs文件夹。
在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导入二进制文件
- 创建nodejs项目。
- 将之前aide项目中libs文件夹复制到nodejs项目中,注意不是jni文件夹中的那个libs,并将libhello.so改名为hello.node。
重点是后缀 .node
- 像官方文档里面一样直接
var $arch = ({
arm: "armeabi-v7a",
arm64: "arm64-v8a"
})[process.arch];
var addon = require(`./libs/${$arch}/hello.node`);
然后不出意外的报错了
很明显,nodejs对于安卓本地存储中的addon没有调用权限,万幸的是aj提供将app内部目录作为脚本文件夹的功能。
- 修改脚本文件为aj内部目录,然后复刻之前在本地存储的操作
怎么把之前的本地文件弄到新目录里面不用说了吧,在aj终端一个cp -r就行。
内部目录下的addon终于可以被nodejs正常导入了
测试api也没有问题
console.log(addon, addon.hello());
大功告成!
ps: 并没有,以上步骤导入的addon会有一个,无法在aj运行期间多次导入的问题,必须是清后台重进aj后的第一次运行js导入才正常,之后再运行js都会导入报错,似乎不少人都遇到过(在正常的nodejs环境中),但在后文中此bug会消失。
在aj导入本地addon文件
- 利用
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) }
- 换回之前本地的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())
再次修改代码
>var addon = requireAddon(`./libs/${$arch}/hello.node`, false); console.log(addon, addon.hello())
看来每个addon文件仍旧只能导入一次,但我们的requireAddon用”巧妙”的办法绕过了这个问题,可能是编译配置仍有不全的地方?
使用nan
- 在终端直接npm i nan,然后把node_modules/nan复制到aide项目的jni/includes中
还有nodejs源码中node-16.x/deps/uv/include里面的内容,也复制到jni/includes/uv里
在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_function_arguments示例中的nan文件夹下的addon.cc,复制到aide项目的jni文件夹中。
再次修改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开始打包就代表编译成功了
libs的架构文件夹里面也出现了libaddon.so修改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);
}
运行成功,没有问题
使用node-addon-api
- 与nan一样的npm i node-addon-api下载后复制到aide项目的jni/includes里面
因为写了头文件批量导入,所以不用再手动添加node-addon-api的头文件导入了
- 在前文的示例存储库中,选择typed_threadsafe_function示例,里面只有一个node-addon-api版本,把clock.cc复制到aide项目的jni文件夹下
在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)
- 开始编译,结果报错了
很明显,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
再次开始编译,成功。
- 在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告诉我,毕竟自己的问题,不能误导了别人。
完。
与君共勉之。