记录阅读v8源码过程的笔记,尝试将V8引擎用JavaScript翻译辅助理解与研究
记录阅读v8源码过程的笔记,尝试用JavaScript翻译V8引擎
找一个文件夹,依次运行
git clone https://github.com/pflhm2005/V8ToJS.git
cd V8ToJS/
npm install babel-cli -g
npm install babel-preset-env -D
准备工作完成
运行对应的文件启动解析(修改文件中的source_code看不一样的输出)
// 输出字符的Token
babel-node --presets env token.js
// 输出字符的抽象语法树
babel-node --presets env ast.js
// 输出字符的字节码(目前测试仅支持let a = 1;这一个,完善需要较大工作量)
babel-node --presets env bytecode.js
let a = 1;let b = {a : 1};if(true){};function fn(a,b,c){log(123);}
等语句解析let a = 1
的字节码生成,复杂语句涉及内存操作基本上无法处理下述内容过于复杂,需要走源码编译看过程,暂未实现。
目前C++部分比较杂乱 没有时间整理,也没啥看头(已移除),看JS文件夹里的内容吧
文件分类跟源码不太一致,逻辑上基本按照源码进行了复现,注释极其详细,更多功能待完善
有一些特殊语法的模拟方式需要说明一下(得益于es6 块级作用域的问题完美解决 不然完蛋了)
语法这块主要是能实现对应功能,不需要刻意保持一致,优先保证逻辑
除去通过zone内部管理内存,V8很多时候也会在栈上实例化类,作用域结束自动析构,JS不支持这种语法,如下:
// 示例代码
template <typename T>
class ScopedPtrList {
public:
explicit ScopedPtrList(std::vector<void*>* buffer): buffer_(*buffer), start_(buffer->size()) {}
~ScopedPtrList() { buffer_.resize(start_); }
private:
std::vector<void*>& buffer_;
size_t start_;
}
vector<void*>* pointer_buffer_;
{
ScopedPtrList<Statement> statements(pointer_buffer_);
// do something...
// 析构在return之后 这样可以同时保证返回及清理
return statements;
}
上述逻辑在JS无法实现
// 可能已经有一些值
let pointer_buffer_ = [];
function handle() {
// 构造很容易模拟
let start_ = pointer_buffer_.length;
let statements = pointer_buffer_;
// do something
// 假设在返回之前做析构操作 会由于引用类型 两者同时被清理
pointer_buffer_.length = start_; // statements也会被重置
return statements;
// 非常尴尬的是 C++的析构是在返回之后 而这里的代码在JS不会执行
pointer_buffer_.length = start_; // never run
}
源码应用中其中一个场景如下
// 假设这里是顶层作用域 pointer_buffer_ = []
function topLevelFunction() {
// 生成一个ScopedPtrList
// 当前作用域有两个变量
// pointer_buffer_ = [a, b]
let a = 1;
let b = 2;
{
// 此时生成新的ScopedPtrList
// pointer_buffer_ = [a, b, c]
let c = 3;
// 从作用域找到a 输出1
console.log(a);
}
// 作用域结束后重置成[a, b]
{
// 此时生成另的ScopedPtrList
// pointer_buffer_ = [a, b, d]
let d = 4;
}
}
这个问题感觉挺大 还没有完美的模拟办法
目前采用两种方式模拟
let pointer_buffer_ = [];
// 1. 这种情况必须保证对象的存活
// 直接生成一个新的数组 C++是为了节省内存才重复使用同一个指针变量 JS不搞这个
function handle() {
let statements = [];
// do something
return new xxx(statements);
}
// 2. 块级作用域结尾可以直接重置 符合C++的语义
{
let start_ = pointer_buffer_.length;
let statements = pointer_buffer_
// do something
pointer_buffer_.length = start_;
}
enum class InferName { kYes, kNo };
直接声明 特殊情况特殊处理
const kYes = 0;
const kNo = 1;
虽然说最佳实践是将枚举类型作为对象名,枚举值作为key,这样容易分辨且可以保证枚举名不重复
但是JS复杂类型的使用成本较高 非常影响执行速度(枚举值数量极其多) 直接采用声明const变量来模拟枚举
注: 特殊情况主要指以下两种
由于枚举类型较多,目前出现了很多重复的情况,因此已经修改为 类名 + _ + 枚举名 的形式
例如Bytecode::kCallRuntime => Bytecode_Function_id_kCallRuntime,这样可以确保不重复且意义明确
JS不存在宏的概念,目前用了一个简单的node工具展开枚举声明宏
至于复杂的嵌套宏 需要自行理解并翻译为JS函数
void fnc(int* a, bool* b) {
*b = true;
// ...
}
// C++很多时候会将一些基本类型变量的地址作为参数传入方法 值会在函数内部通过解指针操作改变
int a = 1;
bool b = false;
fnc(&a, &b);
if(b) { /* ... */ }
// more
JS的基本类型始终是值传递 采用解构赋值实现上述逻辑
function fnc(a, b) {
// ...
return { a , b };
}
// 1. 一般不声明变量 将初始值直接传入
let { a, b } = fnc(1, false);
// 2. 特殊情况声明伪变量
let _a2 = 1;
let _b2 = true;
let { a2, b2 } = fnc(_a2, _b2);
// 3. 复杂返回
// 有些时候返回的是一个实例 基本类型的值只是顺便被改变
// 此时修改返回值 解构一并处理
fuction fnc2(a, b) {
// ...
let result = {/* 通过某些方法生成的实例 */};
// C++中仅会返回result一个变量
return { a, b, result };
}
let { a, b, result } = fnc2(1, false);
// 如此不会影响后续逻辑
这里的重载包括函数重载与函数的构造重载
一般来说,简单的重载可以直接用默认参数来实现
void Scan() { Scan(Next()) }
void Scan(TokenDesc* next) { /* ... */ }
function Scan(next = this.next_) {
/* ... */
}
但是某些情况重载比较复杂 如下
// 参数数量不一致 构造参数也不一致
ObjectLiteral::Property* NewObjectLiteralProperty(Expression* key, Expression* value, ObjectLiteralProperty::Kind kind,bool is_computed_name) {
return new (zone_) ObjectLiteral::Property(key, value, kind, is_computed_name);
}
ObjectLiteral::Property* NewObjectLiteralProperty(Expression* key, Expression* value, bool is_computed_name) {
return new (zone_) ObjectLiteral::Property(ast_value_factory_, key, value, is_computed_name);
}
/**
* 存在重复的参数 根据参数数量 调整一下顺序
* 由于不清楚参数数量 直接用rest表示
* 实际上可以通过默认参数实现 => (key, value, is_computed_name, extra_paran = ast_value_factory_)
* 但是这样一来外部调用方法参数顺序就需要修改 对于使用率较高的方法来说十分不妥
* 因此 外部调用保持与源码一致 差异化的逻辑处理放在工厂函数内部
*/
function NewObjectLiteralProperty(...args) {
if(args.length === 3) return new Property(ast_value_factory_, ...args);
else return new Property(args[2], args[0], args[1], args[3]);
}
// 类的构造函数也要进行修改
class Property {
constructor(ast_value_factory_or_kind/*这是一个动态参数*/,key, value, is_computed_name) {
if(typeof ast_value_factory_or_kind === 'number') {/*做别的初始化*/}
}
}
极端情况下,存在子类、父类均有多重构造函数(见DeclarationScope),或者根据构造参数决定是否调用其余构造方法的逻辑,太过于复杂。
目前会将实例化的类进行拆分,保留原始类,但是仅挂载一些方法,不作为new的对象,后续考虑更优实现方法。
// 原有的多重载构造函数类 不作为new的对象 方法可以放上面
class DeclarationScope extends Scope {
constructor(...args) {
super(...args);
}
allMethod() {}
}
// 拆分出来额外的实例化类 构造函数参数重新设定 仅仅差异化构造过程 所有方法放原有类上
class FunctionDeclarationScope extends DeclarationScope{
constructor() {
// ...
}
}
class ScriptDeclarationScope extends DeclarationScope{
constructor() {
// ...
}
}
实话说,这个语法无法模拟,但是JS的弱类型帮了不少忙,这一块目前没有碰到问题。
在处理bytecode生成的时候,遇到了极复杂宏+模板的情况,且模板不作为声明类型使用,因此必须做处理,源码如下
template <Bytecode bytecode, AccumulatorUse accumulator_use,
OperandType... operand_types>
class BytecodeNodeBuilder {
public:
template <typename... Operands>
V8_INLINE static BytecodeNode Make(BytecodeArrayBuilder* builder,
Operands... operands) {
static_assert(sizeof...(Operands) <= Bytecodes::kMaxOperands,
"too many operands for bytecode");
builder->PrepareToOutputBytecode<bytecode, accumulator_use>();
return BytecodeNode::Create<bytecode, accumulator_use, operand_types...>(
builder->CurrentSourcePosition(bytecode),
OperandHelper<operand_types>::Convert(builder, operands)...);
}
};
template <Bytecode bytecode, AccumulatorUse accum_use,
OperandType operand0_type, OperandType operand1_type,
OperandType operand2_type, OperandType operand3_type,
OperandType operand4_type>
V8_INLINE static BytecodeNode Create(BytecodeSourceInfo source_info,
uint32_t operand0, uint32_t operand1,
uint32_t operand2, uint32_t operand3,
uint32_t operand4) {
OperandScale scale = OperandScale::kSingle;
scale = std::max(scale, ScaleForOperand<operand0_type>(operand0));
scale = std::max(scale, ScaleForOperand<operand1_type>(operand1));
scale = std::max(scale, ScaleForOperand<operand2_type>(operand2));
scale = std::max(scale, ScaleForOperand<operand3_type>(operand3));
scale = std::max(scale, ScaleForOperand<operand4_type>(operand4));
return BytecodeNode(bytecode, 5, scale, source_info, operand0, operand1,
operand2, operand3, operand4);
}
模板参数作为实际参数使用,所以需要进行处理,处理方法是将模板参数包装为数组,命名为template作为函数的额外参数,如下
class BytecodeNodeBuilder {
static Make(builder, operands, template) {
const [bytecode, accumulator_use] = template;
const operand_types = template.slice(2);
builder.PrepareToOutputBytecode(bytecode, accumulator_use);
let source_info = source_info = builder.CurrentSourcePosition(bytecode);
switch (operands.length) {
case 0:
return BytecodeNode.Create0(bytecode, accumulator_use, source_info);
case 1:
return BytecodeNode.Create1(
bytecode, accumulator_use, source_info,
OperandHelper(operand_types[0], builder, operands[0]), operand_types[0]);
case 2:
// more...
}
}
}
一维指针不需要额外的逻辑,JS的复杂类型跟指针操作并没有太大区别
多维指针无法模拟,但是换个思路。根据所看的libuv、V8源码,大多数情况下多维指针的应用场景是实现某个数据结构(链表、队列),而JS的数组无所不能,一般就忽略具体的指针逻辑了。
当出现取地址作为元数据,此时逻辑无法模仿
虽然JS也有迭代器,但是跟C++的简约++操作差的比较远,这块直接换一个实现思路
vector<void*> vec;
auto it = vec.begin();
while(it++ !== vec.end()) {
// ...
}
let vec = [];
// 需要索引
for(/*...*/)
// 不需要索引
for(let it of vec) {/**/}
在进行位运算计算Hash值时,C++用的是unsigned int,但是JS默认是有符号的,这就会导出下列运算结果不一致
unsigned int n = 111;
n <<= 30; // 3221225472
let n = 111;
n <<= 30; // -1073741824
目前大规模的位运算出现在String的Hash值计算,见StringHasher。
由于两个数的底层是一致的,只是符号类型不一致,所以只需要最后做一次无符号位移。
let n = 111;
n <<= 30; // -1073741824
n >>>= 0; // 3221225472
这个就算了吧,若涉及寄存器或操作系统内核操作,无法实现