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.cpp
的sanityCheck
方法
单独执行编译
首先介绍一下命令行工具,我们要生成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
的值会分别调用processBytecodeFile
和processSourceFiles
,我们看下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个子阶段:
生产AST。这里会先判断如果是commonJS则在 AST生成阶段返回的是一个函数表达式节点,如果不是则返回的是程序节点(具体代码见
ESTree::NodePtr parseJS
)。生成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
- 生成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),最终调用 BytecodeSerializer
的serialize
方法序列化字节码并写入到文件。
低级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生成的文本实际上是字节码模块经过反汇编之后得到的,跟实际运行的字节码是不一样的,具体差别我这里就不分析了,有兴趣的可以自己去阅读源码。