nestscript
v1.0.5
Published
A script nested in JavaScript, dynamically run code in environment without `eval` and `new Function`.
Downloads
23
Readme
nestscript
A script nested in JavaScript, dynamically runs code in environment without eval
and new Function
.
nestscript
可以让你在没有 eval
和 new Function
的 JavaScript 环境中运行二进制指令文件。
原理上就是把 JavaScript 先编译成 nestscript
的 IR 指令,然后把指令编译成二进制的文件。只要在环境中引入使用 JavaScript 编写的 nestscript
的虚拟机,都可以执行 nestscript
的二进制文件。你可以把它用在 Web 前端、微信小程序等场景。
它包含三部分:
- 代码生成器:将 JavaScript 编译成
nestscript
中间指令。 - 汇编器:将中间指令编译成可运行在
nestscript
虚拟机的二进制文件。 - 虚拟机:执行汇编器生成的二进制文件。
理论上你可以将任意语言编译成 nestscript
指令集,但是目前 nestscript
只包含了一个代码生成器,目前支持将 JavaScript 编译成 nestscript
指令。
目前支持单文件 ES5 编译,并且已经成功编译并运行一些经典的 JavaScript 第三方库,例如 moment.js、lodash.js、mqtt.js。并且有日活百万级产品在生产环境使用。
Installation
npm install nestscript
快速尝试
新建一个文件,例如 main.js
:
console.log("hello world")
编译成二进制:
npx nsc compile main.js main
这会将 main.js
编译成 main
二进制文件
通过虚拟机运行二进制:
npx nsc run main
会看到终端输出 hello world
。这个 main
二进制文件,可以在任何一个包含了 nestscript 虚拟机,也就是 dist/vm.js
文件的环境中运行。
例如你可以把这个 main
二进制分发到 CND,然后通过网络下载到包含 dist/vm.js
文件的小程序中动态执行。
Example
为了展示它的作用,我们编译了一个开源的的伪 3D 游戏 javascript-racer。可以通过这个网址查看效果:https://livoras.github.io/nestscript-demo/index.html
查看源代码(nestscript-demo)可以看到,我们在网页中引入了一个虚拟机 vm.js
。游戏的主体逻辑都通过 nestscript 编译成了一个二进制文件 game
,然后通过 fetch
下载这个二进制文件,然后给到虚拟机解析、运行。
<!-- 引入 nestscript 虚拟机 -->
<script src="./nestscript/dist/vm.js"></script>
<!-- 下载二进制文件 `game`,并且用虚拟机运行 -->
<script>
fetch('./game').then((res) => {
res.arrayBuffer().then((data) => {
const vm = createVMFromArrayBuffer(data, window)
vm.run()
})
})
</script>
达到的效果和原来的开源的游戏效果完全一致
- 原来用 JS 运行的效果:http://codeincomplete.com/projects/racer/v4.final.html
- 用虚拟机运行 nestscript 二进制的效果:https://livoras.github.io/nestscript-demo/index.html
demo 原理
编译的过程非常简单,只不过是把原来游戏的几个逻辑文件合并在一起:
cat common.js stats.js main.js > game.js
然后用 nestscript 编译成二进制文件:
nsc compile game.js game
再在 html 中引入虚拟机 vm.js,然后通过网络请求获取 game 二进制文件,接着运行二进制:
<!-- 引入 nestscript 虚拟机 -->
<script src="./nestscript/dist/vm.js"></script>
<!-- 下载二进制文件 `game`,并且用虚拟机运行 -->
<script>
fetch('./game').then((res) => {
res.arrayBuffer().then((data) => {
const vm = createVMFromArrayBuffer(data, window)
vm.run()
})
})
</script>
API
nsc compile [source.js] [binary]
把 JavaScript 文件编译成二进制,例如 npx nsc compile game.js game
。注意,目前仅支持 ES5 的语法。
nsc run [binary]
通过 nestscript 虚拟机运行编译好的二进制,例如 npx nsc run game
createVMFromArrayBuffer(buffer: ArrayBuffer, context: any)
由 dist/vm.js
提供的方法,解析编译好的二进制文件,返回一个虚拟机实例,并且可以准备执行。例如:
const vm = createVMFromArrayBuffer(buffer, context)
buffer
指的是二进制用 JavaScript 的 ArrayBuffer 的展示形式;
context
相当于传给虚拟机的一个全局运行环境,因为虚拟机的运行环境和外部分离开来的。它对 window、global 这些已有的 JavaScript 环境无知,所以需要手动传入一个 context
来告知虚拟机目前的全局环境。虚拟机的全局变量、属性都会从传入的 contenxt
中拿到。
例如,如果代码只用到全局的 Date
属性,那么除了可以直接传入 window
对象以外,还可以这么做:
const vm = createVMFromArrayBuffer(buffer, { Date })
VirtualMachine::run()
createVMFromArrayBuffer
返回的虚拟机实例有 run
方法可以运行代码:
const vm = createVMFromArrayBuffer(buffer, { Date })
vm.run()
nestscript 指令集
MOV, ADD, SUB, MUL, DIV, MOD,
EXP, NEG, INC, DEC,
LT, GT, EQ, LE, GE, NE,
LG_AND, LG_OR, XOR, NOT, SHL, SHR,
JMP, JE, JNE, JG, JL, JIF, JF,
JGE, JLE, PUSH, POP, CALL, PRINT,
RET, PAUSE, EXIT,
CALL_CTX, CALL_VAR, CALL_REG, MOV_CTX, MOV_PROP,
SET_CTX,
NEW_OBJ, NEW_ARR, SET_KEY,
FUNC, ALLOC,
详情请见 nestscript 指令集手册。
例如使用指令编写的,斐波那契数列:
func @@main() {
CLS @fibonacci;
REG %r0;
FUNC $RET @@f0;
FUNC @fibonacci @@f0;
MOV %r0 10;
PUSH %r0;
CALL_REG @fibonacci 1 false;
}
func @@f0(.n) {
REG %r0;
REG %r1;
REG %r2;
REG %r3;
MOV %r0 .n;
MOV %r1 1;
LT %r0 %r1;
JF %r0 _l1_;
MOV %r1 0;
MOV $RET %r1;
RET;
JMP _l0_;
LABEL _l1_:
LABEL _l0_:
MOV %r0 .n;
MOV %r1 2;
LE %r0 %r1;
JF %r0 _l3_;
MOV %r1 1;
MOV $RET %r1;
RET;
JMP _l2_;
LABEL _l3_:
LABEL _l2_:
MOV %r2 .n;
MOV %r3 1;
SUB %r2 %r3;
PUSH %r2;
CALL_REG @fibonacci 1 false;
MOV %r0 $RET;
MOV %r2 .n;
MOV %r3 2;
SUB %r2 %r3;
PUSH %r2;
CALL_REG @fibonacci 1 false;
MOV %r1 $RET;
ADD %r0 %r1;
MOV $RET %r0;
RET;
}
TODO
- [ ]
export
,import
模块支持 - [ ]
class
支持 - [ ] 中间代码优化
- [x] 基本中间代码优化
- [ ] 属性访问优化
- [ ] 文档:
- [ ] IR 指令手册
- [x] 安装文档
- [x] 使用手册
- [x] 使用 demo
- [x]
null
,undefined
keyword - [x] 正则表达式
- [x] label 语法
- [x]
try catch
- [x] try catch block
- [x] error 对象获取
- [x] ForInStatement
- [x] 支持 function.length
Change Log
2020-10-10
- 函数调用的时候延迟 new Scope 和 scope.fork 可以很好提升性能(~500ms)
2020-10-09
- 性能优化:不使用
XXXBuffer.set
从 buffer 读取指令速度更快
2020-09-18
- 解决 try catch 调用栈退出到 catch 的函数的地方
2020-09-08
- 重新设计闭包、普通变量的实现方式,使用 scope chain、block chain
- 实现块级作用域
- 使用块级作用域实现
error
参数在catch
的使用
2020-08-25
- 闭包的形式应该是:
- FUNC 每次都返回一个新的函数,并且记录上一层的 closure table
- 调用的时候根据旧的 closure table 构建新的 closure table
2020-08-21
- fix 闭包生成的顺序问题
- 编译第三方库 moment.js, moment.min.js, lodash.js, lodash.min.js 成功并把编译加入测试
2020-08-20
- 编译 moment.js 成功
- fix if else 语句的顺序问题
2020-08-19
- ForInStatement
2020-08-14
- 编译 lodash 成功
2020-08-14
arguments
参数支持
2020-08-12
- fix 闭包问题
- 继续编译 lodash:发现没有 try catch 的实现
2020-08-11
- 继续编译 lodash,发现了运行时闭包声明顺序导致无法获取闭包的 bug
2020-08-10
- 编译 lodash 成功(运行失败)
- 给函数参数增加闭包声明
- UpdateExpression 的前后缀表达存储
2020-08-06
- 完成 label 语法:循环、block label
2020-08-05
null
,undefined
- 正则表达式字面量
2020-08-03
while
,do while
,continue
codegen- 更多测试
2020-07-31
- 第一版 optimizer 完成
2020-07-30
- 设计代码优化器的流程图
2020-07-29
- 把操作数的字节数存放在类型的字节末端,让操作数的字节数量可以动态获取
- 对于 Number 类型使用 Float64 来存储,对于其他类型的操作数用 Int32 存储
- 可以较好地压缩二进制程序的大小.
2020-07-27
- 重新设计操作数的生成规则
2020-07-23
- 函数的调用有几种情况
- vm 调用自身函数
- vm 调用外部函数
- 外部调用 vm 的函数
- 所以:
- vm 在调用函数的时候需要区分是那个环境的函数(函数包装 + instanceof)
- 如果是自身的函数,不需要对参数进行操作
- 如果是外部函数,需要把已经入栈的函数出栈再传给外部函数
- 内部函数在被调用的时候,需要区分是那个环境调用的(NumArgs 包装 + instanceof)
- 如果来自己的调用,不需要进行特别的操作
- 如果是来自外部的调用,需要把参数入栈,并且要嵌入内部循环等待虚拟机函数结束以后再返回
- vm 在调用函数的时候需要区分是那个环境的函数(函数包装 + instanceof)
- 让函数可以正确绑定 this,不管是 vm 内部还是外部的
2020-07-22
- 代码生成中表达式结果的处理原则:所有没有向下一层传递 s.r0 的都要处理 s.r0
- 三目运算符 A ? B : C 的 codegen(复用 IfStatement)
2020-07-21
- 自动包装 @@main 函数,这样就不需要主动提供 main 函数,更接近 JS
2020-07-20
- 更多测试
- 完成 +=, -=, *=, /= 操作符
2020-07-17
- 新增测试 & CI
- uinary expression 的实现:+, -, ~, !, void, delete
2020-07-16
- 逻辑表达式 "&&" 和 "||" 的 codegen 和虚拟机实现
- 完成闭包在虚拟机中的实现
2020-07-15
- 完成闭包变量的标记方式:内层函数“污染”外层的方式
- 重构代码生成的方式,使用函数数组延迟代码生成,这样可以在标记完闭包变量以后再进行 codegen
- 设计闭包变量和普通变量的标记方式“@xxx”表示闭包变量,“%xxx”表示普通自动生成的寄存器
- 下一步设计闭包变量在汇编器和虚拟机中的生成和调取机制
2020-07-13
- 设计闭包的实现
- 实现
CALL_REG R0 3
指令,可以把函数缓存到变量中随后进行调用
2020-07-10
- 支持回调函数的 codegen
- 完成基本的 JS -> 汇编的转化和运行
- 循环 codegen
- 三种值的更新和获取
- Identifier
- context
- variables
- Memeber
- Identifier
2020-07-09
- 完成二进制表达式的 codegen
- 完成简单的赋值语句
- 完成对象属性访问的 codegen
- if 语句的 codegen
2020-07-03
- 确定使用动态分配 & 释放寄存器方案
- 表达式计算值存储到寄存器方案,寄存器名称外层生成、传入、释放
2020-06-22
- 开始使用 acorn 解析 ast,准备把 ast 编译成 IR 指令
2020-06-19
FUNC R0 sayHi
: 构建 JS 函数封装sayHi
函数并存放到 R0 寄存器,可以用作 JS 的回调参数,见example/callback.nes
CALL_VAR R0 "forEach" 1
: 调用寄存器里面存值的某个方法MOV_PROP R0 R1 "length"
: 将 R1 寄存器的值的 "length" 的值放到 R0 寄存器中
2020-06-18
- 完成
NEW_ARR R0
: 字面量数组NEW_OBJ R0
: 字面量对象SET_KEY R0 "0" "hello"
: 设置某个寄存器里面的 key value 值CALL_CTX "console" "log" 1
: 调用 ctx 里面的某个函数方法MOV_CTX R0 "console.log"
: 把 ctx 某个值移动到寄存器
2020-06-17
- 完成基本的汇编器和虚拟机
- 完成命令行工具 nsc,可以
nsc compile src dest
将文本代码 -> 二进制文件,并且用nsc run file
执行 - 斐波那契数列计算例子
- 编译打包成第三方包