Hermes源码分析(一)—— 字节码生成

Hermes 版本:0.5.1

源码位于https://github.com/facebook/hermes

一、目录结构

  • Lib : Hermes核心源码
    • BCGen: Hermes使用了llvm 来生成字节码,核心代码位于lib/BCGen/HBC 目录下
    • CompilerDriver: 编译驱动器,生成字节码的主要流程在这里,包括判断输入的类型是源文件还是字节码,如果是源文件,则生成字节码文件,如果本身已经是字节码了,则可以反汇编。
  • Include: 跟Lib相对应的C++头文件
  • API: 对外提供的接口,入口类在hermes/hermes.h/cpp中 ,提供JSI的接口供React-Native框架调用
    • hermes/DebuggerAPI: 用来调试使用,比如打断点,抓取调用栈等,仅Debug模式下开启,Release模式下默认关闭。
    • Jsi/jsi: 对React-Native提供的接口,跟JSCore等引擎保持一致
  • Tools: 可以独立运行的工具,如生成字节码(Hermesc)、dump字节码的工具(hbcdump)等,内部会调用到Lib目录下的一些类
  • Doc: Hermes的设计文档,可以快速了解Hermes的实现机制

二、编译字节码

我们知道Hermes字节码的格式是分段式的,每一段代表不同的信息,以官网提供的例子来说:

const a = 1; 
print(a);

编译后的结果是

Bytecode File Information:
  Bytecode version number: 83
  Source hash: 0000000000000000000000000000000000000000
  Function count: 1
  String count: 2
  String Kind Entry count: 2
  RegExp count: 0
  Segment ID: 0
  CommonJS module count: 0
  CommonJS module count (static): 0
  Bytecode options:
    staticBuiltins: 0
    cjsModulesStaticallyResolved: 0

Global String Table:
s0[ASCII, 0..5]: global
i1[ASCII, 6..10] #A689F65B: print

Function(1 params, 11 registers, 0 symbols):
Offset in debug table: source 0x0000, lexical 0x0000
    GetGlobalObject   r0
    TryGetById        r2, r0, 1, "print"
    LoadConstUndefined r1
    LoadConstUInt8    r0, 1
    Call2             r0, r2, r1, r0
    Ret               r0

Debug filename table:
  0: /tmp/hermes-input.js

Debug file table:
  source table offset 0x0000: filename id 0

Debug source table:
  0x0000  function idx 0, starts at line 1 col 1
    bc 2: line 2 col 1
    bc 13: line 2 col 6
  0x000a  end of debug source table

Debug lexical table:
  0x0000  lexical parent: none, variable count: 0
  0x0002  end of debug lexical table

字节码各个字段的详细释义可以参考我另一篇文章https://www.jianshu.com/p/bb0cc7b20c50
或者直接看官方文档

编译字节码这个过程有两个入口,一个是单独调用tools/Hermesc 执行,另一个是在运行JS代码的时候再执行。

字节码的格式可以参考include/hermes/BCGen/HBC/BytecodeFileFormat.h这个文件中的定义, 注意上面的例子中没有列出来的一项是魔数,这个是写在最终的字节码文件中的,用来校验文件格式是否为hermes 字节码,Bytecode version number也会参与校验,下发的Hermes字节码跟本地版本不一致是不能执行的,具体代码可参见 BytecodeDataProvider.cppsanityCheck方法

单独执行编译

首先介绍一下命令行工具,我们要生成Hermes字节码首先得生成bundle,然后调用hermesc这个程序进行编译。这里有几个参数说明一下: -emit-binary -out 代表字节码的输出路径,- output-source-map代表原始bundle的路径。

npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output ./CodePush/index.android.bundle --sourcemap-output ./CodePush/index.android.bundle.packager.map

./node_modules/hermes-engine/osx-bin/hermesc -emit-binary -out ./CodePush/index.android.bundle.hbc  -output-source-map  ./CodePush/index.android.bundle

接下来我们来看看调用hermesc时具体做了什么。 先找到hermesc的入口文件 --hermesc.cpp,从代码中可以看出hermes的字节码功能主要是借助了LLVM实现

int main(int argc, char **argv) {
  // Normalize the arg vector.
  llvm::InitLLVM initLLVM(argc, argv);
  // Print a stack trace if we signal out.
  llvm::sys::PrintStackTraceOnErrorSignal("Hermes driver");
  llvm::PrettyStackTraceProgram X(argc, argv);
  // Call llvm_shutdown() on exit to print stats and free memory.
  llvm::llvm_shutdown_obj Y;
  llvm::cl::AddExtraVersionPrinter(driver::printHermesCompilerVersion);
  llvm::cl::ParseCommandLineOptions(argc, argv, "Hermes driver\n");

  if (driver::outputFormatFromCommandLineOptions() ==
      OutputFormatKind::Execute) {
    // --help says "Choose output:" so mimic the wording here
    llvm::errs()
        << "Please choose output, e.g. -emit-binary. hermesc does not support -exec.\n";
    llvm::errs() << "Example: hermesc -emit-binary -out myfile.hbc myfile.js\n";
    return EXIT_FAILURE;
  }

  driver::CompileResult res = driver::compileFromCommandLineOptions();//执行编译
  if (res.bytecodeProvider) {
    llvm::errs() << "Execution not supported with hermesc\n";
    assert(
        false &&
        "Execution mode was not checked prior to compileFromCommandLineOptions");
    return EXIT_FAILURE;
  }
  return res.status;
}

接下来看到driver::CompileResult res = driver::compileFromCommandLineOptions();这一行,这里是执行编译的具体指令,他是在CompilerDriver.cpp中实现的。源码中注释比较详细,相信读者从注释中也能看出来大概是在做什么,让我们把目标聚焦到这个函数最后一段:

  if (cl::BytecodeMode) {
    assert(
        fileBufs.size() == 1 && fileBufs[0].size() == 1 &&
        "validateFlags() should enforce exactly one bytecode input file");
    return processBytecodeFile(std::move(fileBufs[0][0].file));
  } else {
    std::shared_ptr context =
        createContext(std::move(resolutionTable), std::move(segmentRanges));
    return processSourceFiles(context, std::move(fileBufs));
  }

这里根据cl::BytecodeMode的值会分别调用processBytecodeFileprocessSourceFiles,我们看下processSourceFiles这个方法。

这个方法的内容比较长,就不完整贴出来了,主要节选几个代码片段看一下:

  llvm::SHA1 hasher;
  for (const auto &entry : fileBufs) {
    for (const auto &fileAndMap : entry.second) {
      const auto &file = fileAndMap.file;
      hasher.update(
          llvm::StringRef(file->getBufferStart(), file->getBufferSize()));
    }
  }
  auto rawFinalHash = hasher.final();
  SHA1 sourceHash{};
  assert(
      rawFinalHash.size() == SHA1_NUM_BYTES && "Incorrect length of SHA1 hash");
  std::copy(rawFinalHash.begin(), rawFinalHash.end(), sourceHash.begin());

这一段是生成sourceCode的哈希值,也就是字节码里面的Source hash,采用的是SHA1算法,哈希值写到rawFinalHash中。

if (context->getUseCJSModules()) 
    // Allow the IR generation function to populate inputSourceMaps to ensure
    // proper source map ordering.
    if (!generateIRForSourcesAsCJSModules(
            M,
            semCtx,
            declFileList,
            std::move(fileBufs),
            sourceMapGen ? &*sourceMapGen : nullptr)) {
      return ParsingFailed;
    }
  } else {
    if (sourceMapGen) {
      for (const auto &filename : cl::InputFilenames) {
        sourceMapGen->addSource(filename == "-" ? "" : filename);
      }
    }

    auto &mainFileBuf = fileBufs[0][0];
    std::unique_ptr sourceMap{nullptr};
    if (mainFileBuf.sourceMap) {
      sourceMap = SourceMapParser::parse(*mainFileBuf.sourceMap);
      if (!sourceMap) {
        // parse() returns nullptr on failure and reports its own errors.
        return InputFileError;
      }
    }

    auto sourceMapTranslator =
        std::make_shared(context->getSourceErrorManager());
    context->getSourceErrorManager().setTranslator(sourceMapTranslator);
    ESTree::NodePtr ast = parseJS(
        context,
        semCtx,
        std::move(mainFileBuf.file),
        std::move(sourceMap),
        sourceMapTranslator);
    if (!ast) {
      return ParsingFailed;
    }
    if (cl::DumpTarget < DumpIR) {
      return Success;
    }
    generateIRFromESTree(ast, &M, declFileList, {});
 }

这一段是解析JS代码,生成AST和IR ,以及对应的sourceMap。可以细分为3个子阶段:

  1. 生产AST。这里会先判断如果是commonJS则在 AST生成阶段返回的是一个函数表达式节点,如果不是则返回的是程序节点(具体代码见ESTree::NodePtr parseJS)。

  2. 生成IR的代码。参见void ESTreeIRGen::doIt()这个方法,里面采用线性的方式遍历AST的节点来生成对应的IR。关于IR的设计,可以参考官方文档https://hermesengine.dev/docs/ir。

通过在Hermesc 命令中添加-dump-ir 参数,我们可以看到前面示例中的代码转变成IR(高级IR)之后的样子:

function global()
frame = []
%BB0:
  %0 = TryLoadGlobalPropertyInst globalObject : object, "print" : string
  %1 = CallInst %0, undefined : undefined, 1 : number
  %2 = ReturnInst %1
function_end
  1. 生成sourceMap。参考SourceMapParser::parse这个方法。如果有对sourceMap不了解的,可以看下这个网站https://sourcemaps.info/spec.html。

生成IR之后,会通过优化器对IR进行优化,包括函数提升、类型推断、指令简化等等,不同的优化级别(cli::OptimizationLevel)会有不一样的效果

 // Run custom optimization pipeline.
  if (!cl::CustomOptimize.empty()) {
    std::vector opts(
        cl::CustomOptimize.begin(), cl::CustomOptimize.end());
    if (!runCustomOptimizationPasses(M, opts)) {
      llvm::errs() << "Invalid custom optimizations selected.\n\n"
                   << PassManager::getCustomPassText();
      return InvalidFlags;
    }
  } else {
    switch (cl::OptimizationLevel) {
      case cl::OptLevel::O0:
        runNoOptimizationPasses(M);
        break;
      case cl::OptLevel::Og:
        runDebugOptimizationPasses(M);
        break;
      case cl::OptLevel::OMax:
        runFullOptimizationPasses(M);
        break;
    }
  }
  
  // In dbg builds, verify the module before we emit bytecode.
  if (cl::VerifyIR) {
    bool failedVerification = verifyModule(M, &llvm::errs());
    if (failedVerification) {
      M.dump();
      return VerificationFailed;
    }
    assert(!failedVerification && "Module verification failed!");
  }
  
  if (cl::DumpTarget == DumpIR) {
    M.dump();
    return Success;
  }

如果我们指定的dumpTarget是IR的话,到这里就结束了,但如果是bytecode,还会继续走字节码的生成过程。

如果没有指定dumpTarget,则会生成可执行的字节码格式,也就是bytecodeModule(参考https://hermesengine.dev/docs/design)

  BytecodeGenerationOptions genOptions{cl::DumpTarget};
  genOptions.optimizationEnabled = cl::OptimizationLevel > cl::OptLevel::Og;
  genOptions.prettyDisassemble = cl::PrettyDisassemble;
  genOptions.basicBlockProfiling = cl::BasicBlockProfiling;
  // The static builtin setting should be set correctly after command line
  // options parsing and js parsing. Set the bytecode header flag here.
  genOptions.staticBuiltinsEnabled = context->getStaticBuiltinOptimization();
  genOptions.padFunctionBodiesPercent = cl::PadFunctionBodiesPercent;

  // If the user requests to output a source map, then do not also emit debug
  // info into the bytecode.
  genOptions.stripDebugInfoSection = cl::OutputSourceMap;

  genOptions.stripFunctionNames = cl::StripFunctionNames;
  
  // If the dump target is None, return bytecode in an executable form.
  if (cl::DumpTarget == Execute) {
    assert(
        !sourceMapGen &&
        "validateFlags() should enforce no source map output for execution");
    return generateBytecodeForExecution(M, genOptions);
  }

如果指定的dumpTarget是DumpByteCode的话,会生成文本格式的字节码,而如果是emitbinary的话,则会序列化生成二进制的字节码文件,详见generateBytecodeForSerialization方法

/// Compile the module \p M with the options \p genOptions, serializing the
/// result to \p OS. If sourceMapGenOrNull is not null, populate it.
/// \return the CompileResult.
/// The corresponding base bytecode will be removed from \baseBytecodeMap.
CompileResult generateBytecodeForSerialization(
    raw_ostream &OS,
    Module &M,
    const BytecodeGenerationOptions &genOptions,
    const SHA1 &sourceHash,
    OptValue range,
    SourceMapGenerator *sourceMapGenOrNull,
    BaseBytecodeMap &baseBytecodeMap) {
  // Serialize the bytecode to the file.
  if (cl::BytecodeFormat == cl::BytecodeFormatKind::HBC) {
    std::unique_ptr baseBCProvider = nullptr;
    auto itr = baseBytecodeMap.find(range ? range->segment : 0);
    if (itr != baseBytecodeMap.end()) {
      baseBCProvider = std::move(itr->second);
      // We want to erase it from the map because unique_ptr can only
      // have one owner.
      baseBytecodeMap.erase(itr);
    }
    auto bytecodeModule = hbc::generateBytecode(
        &M,
        OS,
        genOptions,
        sourceHash,
        range,
        sourceMapGenOrNull,
        std::move(baseBCProvider));

    if (cl::DumpTarget == DumpBytecode) {
      disassembleBytecode(hbc::BCProviderFromSrc::createBCProviderFromSrc(
          std::move(bytecodeModule)));
    }
  } else {
    llvm_unreachable("Invalid bytecode kind");
  }
  return Success;
}

hbc::generateBytecode方法

std::unique_ptr hbc::generateBytecode(
    Module *M,
    raw_ostream &OS,
    const BytecodeGenerationOptions &options,
    const SHA1 &sourceHash,
    OptValue range,
    SourceMapGenerator *sourceMapGen,
    std::unique_ptr baseBCProvider) {
  auto BM = generateBytecodeModule(
      M,
      M->getTopLevelFunction(),
      options,
      range,
      sourceMapGen,
      std::move(baseBCProvider));
  if (options.format == OutputFormatKind::EmitBundle) {
    assert(BM != nullptr);
    BytecodeSerializer BS{OS, options};
    BS.serialize(*BM, sourceHash);
  }
  // Now that the BytecodeFunctions know their offsets into the stream, we can
  // populate the source map.
  if (sourceMapGen)
    BM->populateSourceMap(sourceMapGen);
  return BM;
}

这里会先调用generateBytecodeModule方法将高级IR转换为低级IR,然后通过BytecodeModuleGenerator生成字节码模块并返回(即代码中的BM),最终调用 BytecodeSerializerserialize方法序列化字节码并写入到文件。

低级IR是对高级IR的优化,这是前面例子生成的低级IR:

function global()
frame = []
%BB0:
  %0 = HBCGetGlobalObjectInst
  %1 = TryLoadGlobalPropertyInst %0 : object, "print" : string
  %2 = HBCLoadConstInst undefined : undefined
  %3 = HBCLoadConstInst 1 : number
  %4 = HBCCallNInst %1, %2 : undefined, %3 : number
  %5 = ReturnInst %4
function_end

对比高级IR,可以发现原来一句话表达的内容拆成了几句话,这更接近于汇编语言。最终生成的字节码也跟这个类似。

这里注意一点,dumpByteCode生成的文本实际上是字节码模块经过反汇编之后得到的,跟实际运行的字节码是不一样的,具体差别我这里就不分析了,有兴趣的可以自己去阅读源码。

你可能感兴趣的:(Hermes源码分析(一)—— 字节码生成)