浅析鸿蒙(ark runtime)执行动态代码

众所周知,鸿蒙next的应用运行在ark runtime下,而ark runtime是驱动着鸿蒙应用的基石。ark runtime支持动态类型和静态类型在内的多种编程语言(目前支持js, ts, arkTs等,与传统js runtime不同的是,ark compiler工具链在编译ts源码时,不会像传统js引擎,先将ts转为js代码,再交给js runtime执行,而是在编译ts源码时,会分析推导类型信息,在运行前即可预生成内联缓存从而加速字节码执行)。详细的代码实现以及官方文档大家可以去这里看:arkcompiler仓库

整体上看,ark runtime仍然是符合Ecmascript规范的js虚拟机实现。既然是js虚拟机实现,那大家一定会想到这样执行远程动态代码下发/热修等场景在鸿蒙上岂不是可以遍地开花,分分钟绕过App Gallary的代码签名/审核随意更新客户端代码!

Eval / new Function

提起使用js动态执行代码,大家第一个想起的一定是eval

Eval作为js标准规范的方法,用于执行一段javascript表达式、声明、脚本并返回代码段执行结果。同时使用Function prototype也能达到相同的效果。

果然没有那么简单,还没有编译Dev Eco Studio的静态检查便提示无法在arkts中使用标准库。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这可难不倒大家,众所周知(大声宣扬),arkts的严格的静态检查仅适用于ets文件,只要把代码写在ts文件中,就能愉快的使用ts/js的大部分特性了(甚至any,随心所欲的向任意对象中加入/删除字段、方法等)。

说干就干,代码是不报错了,程序也跑起来了,但是在调用时app果断崩溃,调用eval / new Function分别提示:

Error message:not support eval().
Error message:Not support eval. Forbidden using new Function()/Function().
export class InjectUtil {
  static inject(obj: any) {
    eval("1+1")
    InjectUtil.evalByFunc()
    obj.Nav = null
  }

  static evalByFunc() {
    var fn = Function as any
    return fn('console.log(eval!!!!!!!!!!!!!!!!!!!!!!!!!!!!)')();
  }
}

看来arkcompiler在运行时也屏蔽掉了js可以随意动态执行代码的特性。看来直接使用ts的api是无法达到动态执行代码的目的。

查阅官方文档,已经对ark runtime的这一表现作出了解释:

ArkCompiler前端编译工具链将ArkTS/TS/JS程序预先静态编译为方舟字节码,并且还提供了多重混淆能力的增强,有效地提升了开发者代码资产的安全强度。另外出于安全的考虑,ArkCompiler不支持sloppy模式的JS代码,也不支持eval等运行动态字符串的功能。

在查阅了鸿蒙官方文档后,发现其并未完全堵死ark runtime动态代码的执行能力,在官方支持上仍然提供了使用ndk来开发动态js代码执行的能力。

JSVM-API

JSVM-API是鸿蒙中提供的基于标准JS引擎提供的一套稳定ABI,为开发者提供了一套较为完整的JS引擎能力,包括创建和销毁引擎,执行JS代码,JS/C++交互等关键能力。相当于v8 api的精简版。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里的js engine在官方文档上并未说明是否是ark runtime,但基于目前ark runtime的开源代码,这里js vm的能力应该还是由ark runtime提供(欢迎勘误)。

受限于JSVM-API提供的能力,我们无法获取到主线程所在的jsvm env(也有可能主线程根本不是jsvm),只能通过jsvm创建一个全新的隔离环境,在这个js环境中,我们可以动态的执行js代码。

使用jsvm执行js脚本的主要流程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

核心代码

// jsvm提供的接口头文件
#include "ark_runtime/jsvm.h"

JSVM_VM* vm;
JSVM_ENV* env;

// 创建jsvm虚拟机 通过napi调用
static void createArcJsContext() {
  JSVM_Status status;

  // 初始化 可以设置vm启动参数 argv argc
  JSVM_InitOptions init_options;
  memset(&init_options, 0, sizeof(init_options));
  OH_JSVM_Init(&init_options);

  // 虚拟机实例
  // 可以设置gc相关参数、snapshot bin等
  JSVM_CreateVMOptions options;
  memset(&options, 0, sizeof(options));
  status = OH_JSVM_CreateVM(&options, vm);
  // vm scope。在jsvm中scope 即作用域,用于资源管理,在scope打开期间资源可用,scope关闭后自愿释放(RAII)
  // 类似v8中各种scope的设计,在后续使用中,各种scope可以以raii风格封装方便使用
  JSVM_VMScope vm_scope;
  status = OH_JSVM_OpenVMScope(js_vm, &vm_scope);

  // js env
  js_env = new JSVM_Env;
  status = OH_JSVM_CreateEnv(...);
  // env scope
  status = OH_JSVM_OpenEnvScope(...);
}

static napi_value EvalJS(napi_env env, napi_callback_info info) {
  ...省略...
  // 待运行js代码
  std::string scriptStr = napiValueToString(env, args[1]);
  // scope 用于资源管理
  OH_JSVM_OpenHandleScope(...);
  // 转为jsvm字符串
  OH_JSVM_CreateStringUtf8(...);
  // 编译js脚本
  OH_JSVM_CompileScript(...);
  // 运行脚本
  OH_JSVM_RunScript(...);
  // 获取执行结果(省略...)
  ...省略...
}

// 释放先前创建的各种scope 关闭vm napi调用
static void releaseResources() {
  OH_JSVM_CloseEnvScope(...);
  OH_JSVM_DestroyEnv(...);
  OH_JSVM_CloseVMScope(...);
  OH_JSVM_DestroyVM(...);
}
// index.d.ts 导出napi方法定义
export const createJsCore: (fun: Function) => number;
export const releaseJsCore: (a: number) => void;
export const evaluateJs: (a: number, str: string) => string;
// 创建首个运行环境,并绑定TS回调
const coreId = testNapi.createJsCore(MyCallback);

let sourcecodestr = `
          {
          let a = "hello World";
          consoleinfo(a);
          const mPromise = createPromise();
          mPromise.then((result) => {
          assertEqual(result, 0);
          onJSResultCallback(result, "abc", "v");
          });
          a;
          };`;

// 在首个运行环境中执行JS代码
console.log("TEST evalUateJS :   " + testNapi.evalUateJS(coreId, sourcecodestr));

// 释放运行环境
testNapi.releaseJsCore(coreId);

实测js代码片段可以成功运行编译执行,从应用的主线程调用napi创建jsvm,并执行指定的一段js代码,在jsvm中创建的环境也能调用创建vm时传入的回调函数将结果返回主线程/主vm。

可以正常实现动态代码和app主线程代码的双向调用:

主isolate(主线程) -> napi -> 用户isolate(jsvm)

用户isolate(jsvm) -> napi -> 主isolate(主线程)

优点

  • js执行环境与主线程环境隔离,通过napi进行中转通讯,在jsvm环境中执行代码不会影响到主线程代码执行,相对安全。

缺点

  • 需要手动管理jsvm、相关vm env等,使用较为繁琐,应用前需要对js vm api进行封装。
  • 动态执行的js代码不能使用import等,需为完整逻辑代码。
  • 不能调用主vm中中相关对象/api。
  • 未经编译arc compiler编译优化,可能会有性能损失。
  • 需要写大量的binding相关的胶水代码,繁杂的工作量较大。

NAPI

node的napi将v8底层数据结构全部黑盒化,抽象为统一的接口,使得开发者无需再使用v8相关api操作,与v8解耦,使开发者不会遇到由于v8引擎变更或node版本升级而重新开发的风险。

在鸿蒙中也实现了napi相关规范与接口定义,使开发者可以以napi规范来开发鸿蒙应用的native扩展。

napi_run_script

napi_run_script是napi标准规范中执行js代码的函数,依赖于napi,该方法的调用比较简单,直接上运行代码及调试结果

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在鸿蒙中napi_run_script调用可以成功完成,也返回了napi_ok成功状态的返回码,但是实际上代码并没有执行,脚本运行返回值也被置为null。

查阅官方文档,并没有对napi_run_script这个接口做出说明,猜测ark runtime屏蔽了或未实现这个接口。

napi_run_script_path

针对这个接口,鸿蒙官方有给相关说明:napi_run_script_path官方文档。文档中提到可以使用该接口执行abc格式的文件(arkts编译后生成的字节码产物),但没有给出操作流程。摸索了下,鸿蒙sdk中的es2abc工具可以实现这个目标:
/Applications/DevEco-Studio.app/Contents/sdk/HarmonyOS-NEXT-DB3/openharmony/ets/build-tools/ets-loader/bin/ark/build-mac/bin/es2abc

用法(注意,使用macos的同学需要将es2abc这个文件拷贝出.app, 否则会因为权限不足的原因拷贝失败):

./es2abc test.js

将生成的test.abc文件拷贝至entry的rawfile中,调用即可执行对应的代码。

napi_run_script_path(env, "/entry/resources/rawfile/test.abc", &result);

附上测试脚本test.ets:

console.log('hello world from dynamic code')
console.log(globalThis)
globalThis.Cornerstone.hello()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
可以发现动态下发的代码可以正常调用到我们在程序运行时向globalThis中注入的Cornerstone对象,说明动态执行的代码与app运行的主线程代码处于同一vm环境。

优点

使用napi执行动态代码,无需手动管理vm虚拟机的生命周期,同时可以复用并调用到主vm环境中已存在的对象,更加灵活友好。

可享受到.abc带来的性能优化。

但同时下发的abc文件也与jsvm的限制一致,无法使用import,需将动态代码中的相关依赖提前注入到vm环境/携带完整逻辑等。

缺点

因为执行环境与app主vm一致,恶意代码可随意破环主vm中相关内存数据。

总结

出于安全考虑鸿蒙(ark runtime)在ts层面对动态执行代码作出了诸多限制,但是我们仍能通过napi / jsvm的方式达到动态执行代码的目的,尤其是napi,可在主vm环境中执行动态代码,并且能享受到方舟编译器所带来的性能优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值