项目作者: pflhm2005

项目描述 :
记录阅读v8源码过程的笔记,尝试将V8引擎用JavaScript翻译辅助理解与研究
高级语言: JavaScript
项目地址: git://github.com/pflhm2005/V8ToJS.git
创建时间: 2019-07-16T08:06:05Z
项目社区:https://github.com/pflhm2005/V8ToJS

开源协议:

下载


V8record

记录阅读v8源码过程的笔记,尝试用JavaScript翻译V8引擎


运行方法

找一个文件夹,依次运行

  1. git clone https://github.com/pflhm2005/V8ToJS.git
  2. cd V8ToJS/
  3. npm install babel-cli -g
  4. npm install babel-preset-env -D

准备工作完成

运行对应的文件启动解析(修改文件中的source_code看不一样的输出)

  1. // 输出字符的Token
  2. babel-node --presets env token.js
  3. // 输出字符的抽象语法树
  4. babel-node --presets env ast.js
  5. // 输出字符的字节码(目前测试仅支持let a = 1;这一个,完善需要较大工作量)
  6. babel-node --presets env bytecode.js

简单说明

  • 支持所有Token的解析
  • 支持完成了let a = 1;let b = {a : 1};if(true){};function fn(a,b,c){log(123);}等语句解析
  • 支持let a = 1的字节码生成,复杂语句涉及内存操作基本上无法处理
  • 支持机器码生成

下述内容过于复杂,需要走源码编译看过程,暂未实现。

  • template_string 模板字符串 Token解析阶段有点没明白
  • preParse/lazy-compile 懒编译 针对函数声明代码块的懒编译,过程非常复杂
  • class 类的解析比想象中复杂 另外V8最近重写了这块内容 还没来得及看
  • yield/async 状态保留函数类型有些复杂

目前C++部分比较杂乱 没有时间整理,也没啥看头(已移除),看JS文件夹里的内容吧

文件分类跟源码不太一致,逻辑上基本按照源码进行了复现,注释极其详细,更多功能待完善


有一些特殊语法的模拟方式需要说明一下(得益于es6 块级作用域的问题完美解决 不然完蛋了)

语法这块主要是能实现对应功能,不需要刻意保持一致,优先保证逻辑


析构

除去通过zone内部管理内存,V8很多时候也会在栈上实例化类,作用域结束自动析构,JS不支持这种语法,如下:

  1. // 示例代码
  2. template <typename T>
  3. class ScopedPtrList {
  4. public:
  5. explicit ScopedPtrList(std::vector<void*>* buffer): buffer_(*buffer), start_(buffer->size()) {}
  6. ~ScopedPtrList() { buffer_.resize(start_); }
  7. private:
  8. std::vector<void*>& buffer_;
  9. size_t start_;
  10. }
  11. vector<void*>* pointer_buffer_;
  12. {
  13. ScopedPtrList<Statement> statements(pointer_buffer_);
  14. // do something...
  15. // 析构在return之后 这样可以同时保证返回及清理
  16. return statements;
  17. }

上述逻辑在JS无法实现

  1. // 可能已经有一些值
  2. let pointer_buffer_ = [];
  3. function handle() {
  4. // 构造很容易模拟
  5. let start_ = pointer_buffer_.length;
  6. let statements = pointer_buffer_;
  7. // do something
  8. // 假设在返回之前做析构操作 会由于引用类型 两者同时被清理
  9. pointer_buffer_.length = start_; // statements也会被重置
  10. return statements;
  11. // 非常尴尬的是 C++的析构是在返回之后 而这里的代码在JS不会执行
  12. pointer_buffer_.length = start_; // never run
  13. }

源码应用中其中一个场景如下

  1. // 假设这里是顶层作用域 pointer_buffer_ = []
  2. function topLevelFunction() {
  3. // 生成一个ScopedPtrList
  4. // 当前作用域有两个变量
  5. // pointer_buffer_ = [a, b]
  6. let a = 1;
  7. let b = 2;
  8. {
  9. // 此时生成新的ScopedPtrList
  10. // pointer_buffer_ = [a, b, c]
  11. let c = 3;
  12. // 从作用域找到a 输出1
  13. console.log(a);
  14. }
  15. // 作用域结束后重置成[a, b]
  16. {
  17. // 此时生成另的ScopedPtrList
  18. // pointer_buffer_ = [a, b, d]
  19. let d = 4;
  20. }
  21. }

这个问题感觉挺大 还没有完美的模拟办法

目前采用两种方式模拟

  1. let pointer_buffer_ = [];
  2. // 1. 这种情况必须保证对象的存活
  3. // 直接生成一个新的数组 C++是为了节省内存才重复使用同一个指针变量 JS不搞这个
  4. function handle() {
  5. let statements = [];
  6. // do something
  7. return new xxx(statements);
  8. }
  9. // 2. 块级作用域结尾可以直接重置 符合C++的语义
  10. {
  11. let start_ = pointer_buffer_.length;
  12. let statements = pointer_buffer_
  13. // do something
  14. pointer_buffer_.length = start_;
  15. }

枚举

  1. enum class InferName { kYes, kNo };

直接声明 特殊情况特殊处理

  1. const kYes = 0;
  2. const kNo = 1;

虽然说最佳实践是将枚举类型作为对象名,枚举值作为key,这样容易分辨且可以保证枚举名不重复

但是JS复杂类型的使用成本较高 非常影响执行速度(枚举值数量极其多) 直接采用声明const变量来模拟枚举

注: 特殊情况主要指以下两种

  1. Token类型的枚举,由于Token值在整个AST中解析中非常关键,所以显式的用字符串来表示,让后续复盘逻辑更加清晰。后果是在很多进行类型判断的运算中,需要用一个TokenEnumList手动将枚举字符串还原成Token对应的数值。
  2. Token重名,重名有两种情况。(1)该枚举值仅仅在小范围内使用,例如关于函数返回类型的kNormal、kAsyncReturn(待定)。(2)多个重名枚举值存在较为广泛的应用,将更高级的以下划线开头,例如抽象树节点类型NodeType。

由于枚举类型较多,目前出现了很多重复的情况,因此已经修改为 类名 + _ + 枚举名 的形式

例如Bytecode::Function_id::kCallRuntime => Bytecode_Function_id_kCallRuntime,这样可以确保不重复且意义明确


JS不存在宏的概念,目前用了一个简单的node工具展开枚举声明宏

至于复杂的嵌套宏 需要自行理解并翻译为JS函数


基本类型的引用传递

  1. void fnc(int* a, bool* b) {
  2. *b = true;
  3. // ...
  4. }
  5. // C++很多时候会将一些基本类型变量的地址作为参数传入方法 值会在函数内部通过解指针操作改变
  6. int a = 1;
  7. bool b = false;
  8. fnc(&a, &b);
  9. if(b) { /* ... */ }
  10. // more

JS的基本类型始终是值传递 采用解构赋值实现上述逻辑

  1. function fnc(a, b) {
  2. // ...
  3. return { a , b };
  4. }
  5. // 1. 一般不声明变量 将初始值直接传入
  6. let { a, b } = fnc(1, false);
  7. // 2. 特殊情况声明伪变量
  8. let _a2 = 1;
  9. let _b2 = true;
  10. let { a2, b2 } = fnc(_a2, _b2);
  11. // 3. 复杂返回
  12. // 有些时候返回的是一个实例 基本类型的值只是顺便被改变
  13. // 此时修改返回值 解构一并处理
  14. fuction fnc2(a, b) {
  15. // ...
  16. let result = {/* 通过某些方法生成的实例 */};
  17. // C++中仅会返回result一个变量
  18. return { a, b, result };
  19. }
  20. let { a, b, result } = fnc2(1, false);
  21. // 如此不会影响后续逻辑

重载(运算符重载没什么意义)

这里的重载包括函数重载与函数的构造重载
一般来说,简单的重载可以直接用默认参数来实现

  1. void Scan() { Scan(Next()) }
  2. void Scan(TokenDesc* next) { /* ... */ }
  1. function Scan(next = this.next_) {
  2. /* ... */
  3. }

但是某些情况重载比较复杂 如下

  1. // 参数数量不一致 构造参数也不一致
  2. ObjectLiteral::Property* NewObjectLiteralProperty(Expression* key, Expression* value, ObjectLiteralProperty::Kind kind,bool is_computed_name) {
  3. return new (zone_) ObjectLiteral::Property(key, value, kind, is_computed_name);
  4. }
  5. ObjectLiteral::Property* NewObjectLiteralProperty(Expression* key, Expression* value, bool is_computed_name) {
  6. return new (zone_) ObjectLiteral::Property(ast_value_factory_, key, value, is_computed_name);
  7. }
  1. /**
  2. * 存在重复的参数 根据参数数量 调整一下顺序
  3. * 由于不清楚参数数量 直接用rest表示
  4. * 实际上可以通过默认参数实现 => (key, value, is_computed_name, extra_paran = ast_value_factory_)
  5. * 但是这样一来外部调用方法参数顺序就需要修改 对于使用率较高的方法来说十分不妥
  6. * 因此 外部调用保持与源码一致 差异化的逻辑处理放在工厂函数内部
  7. */
  8. function NewObjectLiteralProperty(...args) {
  9. if(args.length === 3) return new Property(ast_value_factory_, ...args);
  10. else return new Property(args[2], args[0], args[1], args[3]);
  11. }
  12. // 类的构造函数也要进行修改
  13. class Property {
  14. constructor(ast_value_factory_or_kind/*这是一个动态参数*/,key, value, is_computed_name) {
  15. if(typeof ast_value_factory_or_kind === 'number') {/*做别的初始化*/}
  16. }
  17. }

极端情况下,存在子类、父类均有多重构造函数(见DeclarationScope),或者根据构造参数决定是否调用其余构造方法的逻辑,太过于复杂。

目前会将实例化的类进行拆分,保留原始类,但是仅挂载一些方法,不作为new的对象,后续考虑更优实现方法。

  1. // 原有的多重载构造函数类 不作为new的对象 方法可以放上面
  2. class DeclarationScope extends Scope {
  3. constructor(...args) {
  4. super(...args);
  5. }
  6. allMethod() {}
  7. }
  8. // 拆分出来额外的实例化类 构造函数参数重新设定 仅仅差异化构造过程 所有方法放原有类上
  9. class FunctionDeclarationScope extends DeclarationScope{
  10. constructor() {
  11. // ...
  12. }
  13. }
  14. class ScriptDeclarationScope extends DeclarationScope{
  15. constructor() {
  16. // ...
  17. }
  18. }

泛型

实话说,这个语法无法模拟,但是JS的弱类型帮了不少忙,这一块目前没有碰到问题。

在处理bytecode生成的时候,遇到了极复杂宏+模板的情况,且模板不作为声明类型使用,因此必须做处理,源码如下

  1. template <Bytecode bytecode, AccumulatorUse accumulator_use,
  2. OperandType... operand_types>
  3. class BytecodeNodeBuilder {
  4. public:
  5. template <typename... Operands>
  6. V8_INLINE static BytecodeNode Make(BytecodeArrayBuilder* builder,
  7. Operands... operands) {
  8. static_assert(sizeof...(Operands) <= Bytecodes::kMaxOperands,
  9. "too many operands for bytecode");
  10. builder->PrepareToOutputBytecode<bytecode, accumulator_use>();
  11. return BytecodeNode::Create<bytecode, accumulator_use, operand_types...>(
  12. builder->CurrentSourcePosition(bytecode),
  13. OperandHelper<operand_types>::Convert(builder, operands)...);
  14. }
  15. };
  16. template <Bytecode bytecode, AccumulatorUse accum_use,
  17. OperandType operand0_type, OperandType operand1_type,
  18. OperandType operand2_type, OperandType operand3_type,
  19. OperandType operand4_type>
  20. V8_INLINE static BytecodeNode Create(BytecodeSourceInfo source_info,
  21. uint32_t operand0, uint32_t operand1,
  22. uint32_t operand2, uint32_t operand3,
  23. uint32_t operand4) {
  24. OperandScale scale = OperandScale::kSingle;
  25. scale = std::max(scale, ScaleForOperand<operand0_type>(operand0));
  26. scale = std::max(scale, ScaleForOperand<operand1_type>(operand1));
  27. scale = std::max(scale, ScaleForOperand<operand2_type>(operand2));
  28. scale = std::max(scale, ScaleForOperand<operand3_type>(operand3));
  29. scale = std::max(scale, ScaleForOperand<operand4_type>(operand4));
  30. return BytecodeNode(bytecode, 5, scale, source_info, operand0, operand1,
  31. operand2, operand3, operand4);
  32. }

模板参数作为实际参数使用,所以需要进行处理,处理方法是将模板参数包装为数组,命名为template作为函数的额外参数,如下

  1. class BytecodeNodeBuilder {
  2. static Make(builder, operands, template) {
  3. const [bytecode, accumulator_use] = template;
  4. const operand_types = template.slice(2);
  5. builder.PrepareToOutputBytecode(bytecode, accumulator_use);
  6. let source_info = source_info = builder.CurrentSourcePosition(bytecode);
  7. switch (operands.length) {
  8. case 0:
  9. return BytecodeNode.Create0(bytecode, accumulator_use, source_info);
  10. case 1:
  11. return BytecodeNode.Create1(
  12. bytecode, accumulator_use, source_info,
  13. OperandHelper(operand_types[0], builder, operands[0]), operand_types[0]);
  14. case 2:
  15. // more...
  16. }
  17. }
  18. }

多维指针

一维指针不需要额外的逻辑,JS的复杂类型跟指针操作并没有太大区别

多维指针无法模拟,但是换个思路。根据所看的libuv、V8源码,大多数情况下多维指针的应用场景是实现某个数据结构(链表、队列),而JS的数组无所不能,一般就忽略具体的指针逻辑了。

当出现取地址作为元数据,此时逻辑无法模仿


迭代器

虽然JS也有迭代器,但是跟C++的简约++操作差的比较远,这块直接换一个实现思路

  1. vector<void*> vec;
  2. auto it = vec.begin();
  3. while(it++ !== vec.end()) {
  4. // ...
  5. }
  1. let vec = [];
  2. // 需要索引
  3. for(/*...*/)
  4. // 不需要索引
  5. for(let it of vec) {/**/}

无符号整数

在进行位运算计算Hash值时,C++用的是unsigned int,但是JS默认是有符号的,这就会导出下列运算结果不一致

  1. unsigned int n = 111;
  2. n <<= 30; // 3221225472
  1. let n = 111;
  2. n <<= 30; // -1073741824

目前大规模的位运算出现在String的Hash值计算,见StringHasher

由于两个数的底层是一致的,只是符号类型不一致,所以只需要最后做一次无符号位移。

  1. let n = 111;
  2. n <<= 30; // -1073741824
  3. n >>>= 0; // 3221225472

内存管理

这个就算了吧,若涉及寄存器或操作系统内核操作,无法实现