使用JS實現JS編譯器,並將目標js生成二進位制
上一篇文章 ofollow,noindex">利用LLVM實現JS的編譯器,創造屬於自己的語言 中講到使用llvm用C實現JS編譯器,本片文章將使用JS來實現JS編譯器,還是應了《Atwood定律》能夠使用JavaScript實現的,必將使用JavaScript實現。本片文章C程式碼不超過10行,即使完全不會C也可以相對容易的閱讀。
本文程式碼倉庫地址: 點這裡
本次使用npm庫
@babel/core
直接使用@babel/core來進行詞法分析和生成ast。
llvm-node
使用本庫將ast樹繫結生成IR(中間程式碼)和編譯成二進位制檔案,雖然這個庫看起來沒什麼文件,但好用api基本寫在了llvm-node.d.ts裡面。題外話:講真我挺喜歡用ts寫的庫,但我個人不喜歡寫ts,當然這並不矛盾。
使用@babel/core進行解析
講真這個挺好用的,解析JS就幾行程式碼就能生成ast,爽歪歪,具體結構不展開,自己可以嘗試一下。程式碼也就幾行,應該一看就明白了。
//parse.js const babel_core = require('@babel/core'); const fs= require('fs'); module.exports = function (js_path){ let js_conent = fs.readFileSync(js_path); let js_ast = babel_core.parse(js_conent); return js_ast; } 複製程式碼
將解析的的AST繫結IR,實現編譯器
語法解析開始,針對不通的型別進入不通的入口進行解析
//jsvm.js class JSVM{ ... handler(node,parent_node = null) { switch(node.type) { case 'Program': // 這是主程式入口 return this.programHandler(node); case 'FunctionDeclaration': // 當是方法的型別走這裡 return this.functionHandler(node); case 'BlockStatement': // 程式碼塊型別走這裡 return this.blockHandler(node); case 'IfStatement': // IF塊型別走這裡 return this.ifHandler(node); case 'BinaryExpression': // 二進位制表示式型別走這裡 return this.binaryHandler(node); case 'ReturnStatement': // 解析返回 return this.returnHandler(node); case 'Identifier': // 變數或函式呼叫,需要通過父節點判斷,所以傳入 return this.identifierHandler(node,parent_node); case 'NumericLiteral': //數字型別走這 return this.numberHandler(node); case 'StringLiteral': //文字型別走這 return this.stringHandler(node); case 'CallExpression': // 函式呼叫走著 return this.callHandler(node); case 'ExpressionStatement': // 表示式型別走這 return this.expressionHandler(node); default: // 目前不支援的型別直接拋錯 throw new Error('not support grammar type'); } } // 入口檔案 gen() { // 初始化變數和方法包括C擴充套件 this.init(); // 將ast進行解析和繫結 this.handler(this.js_ast.program); } // 對程式主題不斷解析下一個語法節點就好 programHandler(node) { for(let i=0;i<node.body.length;i++) { this.handler(node.body[i]); } } ... } 複製程式碼
以函式繫結舉例
//jsvm.js functionHandler(node) { // 拿到函式節點的函式名 let func_name = node.id.name; // 判斷模組中函式是否存在 the_function = the_module.getFunction(func_name); if (the_function) { throw new Error('function is exist'); } // 設定返回值,目前先定死為double型別 let double_type = llvm.Type.getDoubleTy(the_context); // 設定引數,目前先定死為double型別 let params_list = []; for(let i=0;i<node.params.length;i++) { params_list.push(double_type); } // 把引數注入,生成函式型別 let the_function_type = llvm.FunctionType.get( double_type,params_list,false ); // 創造出一個函式 the_function = llvm.Function.create( the_function_type, llvm.LinkageTypes.ExternalLinkage, func_name,the_module ); // 將引數的名稱插入 let the_args = the_function.getArguments(); for(let i=0;i<the_args.length;i++) { the_args[i].name=node.params[i].name; } // 建立函式主執行節點 let basic_block = llvm.BasicBlock.create(the_context,"entry",the_function); // 設定程式碼插入位置,這個basic_block就是entry節點 builder.setInsertionPoint(basic_block); // 這裡是為了註冊變數,使得在函式作用域內變數可用 variable_map = {}; for(let i=0;i<the_args.length;i++) { variable_map[the_args[i].name]=the_args[i]; } // 判斷函式是否是塊表示式,不是則踢出 if (node.body.type!='BlockStatement') { throw new Error('function body only support BlockStatement'); } // 呼叫解析塊表示式的方法 this.blockHandler(node.body); // 校驗函式是否正確,不正確這個函式會直接報錯 llvm.verifyFunction(the_function); return the_function; } 複製程式碼
塊表示式解析的實現
其實這非同步就是遍歷節點進行解析,是不是很簡單
//jsvm.js blockHandler(node) { let expr_list = []; for(let i=0;i<node.body.length;i++) { expr_list.push(this.handler(node.body[i])); } return expr_list; } 複製程式碼
以IF的解析實現來講程式碼塊的跳躍
//jsvm.js ifHandler(node) { //判斷條件的型別是否是二進位制表示式 if (node.test.type!='BinaryExpression') { throw new Error('if conds only support binary expression'); } // 解析二進位制表示式作為條件 let cond = this.binaryHandler(node.test); // 生成數字0 let zero = llvm.ConstantFP.get(the_context,0); // 如果cond不是bool型別的指,將它轉換為bool型別的值 let cond_v = builder.createFCmpONE(cond,zero,"ifcond"); // 建立then和else和ifcont程式碼塊,實際就是程式碼塊標籤 let then_bb = llvm.BasicBlock.create(the_context,"then",the_function); let else_bb = llvm.BasicBlock.create(the_context,"else",the_function); let phi_bb = llvm.BasicBlock.create(the_context, "ifcont",the_function); // 創造條件判斷 // 如果cond_v是真就跳躍到then_bb程式碼塊,否則跳躍到else_bb程式碼塊 builder.createCondBr(cond_v,then_bb,else_bb); // 設定往then_bb程式碼塊寫入內容 builder.setInsertionPoint(then_bb); if (!node.consequent) {throw new Error('then not extist');} if (node.consequent.type!='BlockStatement') { throw new Error('then body only support BlockStatement'); } // 解析程式碼塊 let then_value_list = this.blockHandler(node.consequent); // 如果程式碼塊沒內容就就跳躍到phi_bb程式碼塊 if (then_value_list.length==0) { builder.createBr(phi_bb); } // 設定往else_bb程式碼塊寫入內容,和then_else差不多 // 不同點:else允許沒有 builder.setInsertionPoint(else_bb); let else_value_list =[]; if (node.alternate) { if (node.alternate.type!='BlockStatement') { throw new Error('else body only support BlockStatement'); } else_value_list = this.blockHandler(node.alternate); } if (else_value_list.length==0) { builder.createBr(phi_bb); } // 因為無論是then或else如果不中斷一定會往phi_bb程式碼塊 // 所以後續的程式碼直接在phi_bb裡面寫就好 builder.setInsertionPoint(phi_bb); } 複製程式碼
支援C擴充套件的實現
首先先定義存在值
//jsvm.js // 定義一個C函式printDouble用於列印二進位制變數 getPrintDouble() { // 獲取返回值型別 let double_type = llvm.Type.getDoubleTy(the_context) // 設定引數列表 let params_list = [double_type]; // 獲取函式型別 let the_function_type = llvm.FunctionType.get( double_type,params_list,false ); // 建立函式定義 the_function = llvm.Function.create( the_function_type, llvm.LinkageTypes.ExternalLinkage, 'printDouble',the_module ); // 設定引數名稱 let the_args = the_function.getArguments(); the_args[0].name = "double_name"; return the_function; } // 初始化方法值講需要預置的方法放入 init() { init_function_map.printDouble = this.getPrintDouble(); } 複製程式碼
C程式碼的實現printDouble方法
// printDouble.cpp #include <stdio.h> // 問什麼要要加extern "C" ,因為c++編譯的時候會自動進行函式簽名 // 如果沒有extern "C" ,彙編裡的方法名就會是Z11printDoubled // 其中籤名前部分由返回值和名稱空間名字中間是方法名,後面是引數縮寫 extern "C" { // 設定返回值和引數都是double型別 double printDouble(double double_num) { // 列印double型別 printf("double_num is: %f\r\n",double_num); // 返回double型別 return double_num; } } 複製程式碼
看看實現效果
要被編譯的程式碼
// fibo.js 這是斐波納切數 function fibo(num) { if (num<=2) {return 1;} return fibo(num-1)+fibo(num-2); } // 講main作為主函式執行 function main() { return printDouble(fibo(9)); } 複製程式碼
開始編譯,並生成中間程式碼和bitcode程式碼,如下
# index.js是編譯器入口,fibo.js是要被編譯的函式 node index.js fibo.js 複製程式碼

將bitcode程式碼生成彙編程式碼
llc fibo.js.bc -o fibo.s 複製程式碼
將彙編程式碼和我們要注入的C程式碼一起編譯
當然除了C只要能被gcc編譯成彙編的也都支援作為擴充套件語言,本文舉例C程式碼容易讓人理解
gcc printDouble.cpp fibo.s -o fibo 複製程式碼
最後執行看看
./fibo 複製程式碼

總結
這次實現是用純JS就能實現,如果後續這個JSVM能編譯覆蓋所有的編譯器自身所有的程式碼功能,理論上來說可以用JSVM編譯JSVM實現自舉,當然這個是一個浩大的工程,方法是有了缺的只是時間而已。