这篇文章基于的Piccolo版本的commit号为214542257eeda28b09495fe38910d42fa2ba48b4
项目的构建官方给出的视频已经有所介绍,这里就不再赘述了,需要注意的是windows系统建议使用MSVC,最好不要尝试使用MinGW的gcc或clang,因为依赖的第三方库JoltPhysics的代码里会根据你是windows而假定你使用MSVC,光是修改Jolt的CMakeLists似乎不太能使它正确通过编译。
Piccolo的编译运行主要分为以下几个部分
生成PiccoloParser
使用PiccoloParser读取项目源代码文件,在build目录下生成所有被include的头文件parser_header.h,并基于此生成反射所需的头文件,生成的文件在engine/source/_generated目录下,这部分被称为Precompile
根据生成好的反射头文件和项目源文件,生成可执行引擎程序
这里我们直接看engine目录下的CMakeLists.txt,前面都是些准备工作,关键是最后这段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 add_subdirectory (shader)add_subdirectory (3 rdparty)add_subdirectory (source/runtime)add_subdirectory (source/editor)add_subdirectory (source/meta_parser)set (CODEGEN_TARGET "PiccoloPreCompile" )include (source/precompile/precompile.cmake)set_target_properties ("${CODEGEN_TARGET}" PROPERTIES FOLDER "Engine" )add_dependencies (PiccoloRuntime "${CODEGEN_TARGET}" )add_dependencies ("${CODEGEN_TARGET}" "PiccoloParser" )
这里通过add_subdirectory
和include
,添加了各个target,包括我们前面提到的precompile,PiccoloRuntime以及PiccoloParser。通过add_dependencies,cmake定义了各个target之间的依赖关系,从而决定了执行顺序,PiccoloParser
> CODEGEN_TARGET > PiccoloRuntime。
这里我们再稍微看一眼precompile.cmake,可以看到它最后一部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 add_custom_target (${PRECOMPILE_TARGET} ALLCOMMAND ${CMAKE_COMMAND} -E echo "************************************************************* " COMMAND ${CMAKE_COMMAND} -E echo "**** [Precompile] BEGIN " COMMAND ${CMAKE_COMMAND} -E echo "************************************************************* " COMMAND ${PRECOMPILE_PARSER} "${PICCOLO_PRECOMPILE_PARAMS_PATH}" "${PARSER_INPUT}" "${ENGINE_ROOT_DIR}/source" ${sys_include} "Piccolo" 0 COMMAND ${CMAKE_COMMAND} -E echo "+++ Precompile finished +++" )
通过这个custom_target的COMMAND ${PRECOMPILE_PARSER} "${PICCOLO_PRECOMPILE_PARAMS_PATH}" "${PARSER_INPUT}" "${ENGINE_ROOT_DIR}/source" ${sys_include} "Piccolo" 0
,
它使用在它之前生成的PiccoloParser.exe对项目include进行解析,其中"${PARSER_INPUT}"
就是前面提到的parser_header.h,虽然这个变量名有点怪,看上去是输入,但实际上是precompile的输出。这里我们先看一眼parser_header.h的大致结构
1 2 3 4 5 6 #ifndef __PARSER_HEADER_H__ #define __PARSER_HEADER_H__ #include "D:/CppProj/Piccolo/engine/source/runtime/core/base/hash.h" #include "D:/CppProj/Piccolo/engine/source/runtime/core/base/macro.h" " #endif
那么PiccoloParser是怎么工作的呢?跟一下代码,可以看到它的主要功能函数在engine/source/meta_parser/parser/parser.cpp,这个parser里的函数的参数名命名也感觉有点怪,不过不影响我们理解代码。下面代码的m_source_include_file_name
就是parser_header.h。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 include_file.open (m_source_include_file_name, std::ios::out);if (!include_file.is_open ()) { std::cout << "Could not open the Source Include file: " << m_source_include_file_name << std::endl; return false ; } std::cout << "Generating the Source Include file: " << m_source_include_file_name << std::endl; std::string output_filename = Utils::getFileName (m_source_include_file_name);if (output_filename.empty ()) { output_filename = "META_INPUT_HEADER_H" ; }else { Utils::replace (output_filename, "." , "_" ); Utils::replace (output_filename, " " , "_" ); Utils::toUpper (output_filename); } include_file << "#ifndef __" << output_filename << "__" << std::endl; include_file << "#define __" << output_filename << "__" << std::endl;for (auto include_item : inlcude_files) { std::string temp_string (include_item) ; Utils::replace (temp_string, '\\' , '/' ); include_file << "#include \"" << temp_string << "\"" << std::endl; } include_file << "#endif" << std::endl; include_file.close ();
代码还是非常直观的,可以看出我们的分析没有错,就是生成parser_header.h,其中include_files就是类似这样的字符串
1 D:/CppProj/ Piccolo/engine/ source /runtime/ core/base/ hash.h;D:/CppProj/ Piccolo/engine/ source /runtime/ core/base/m acro.h;D:/CppProj/ Piccolo/engine/ source /runtime/ core/color/ color.h;D:/CppProj/ Piccolo/engine/ source /runtime/ core/log/ log_system.h;
这个字符串读自engine/bin/precompile.json,那么这个文件是怎么生成的呢?
在precompile.cmake开头我们可以看到这样的代码
1 2 3 4 set (PRECOMPILE_TOOLS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/bin" )set (PICCOLO_PRECOMPILE_PARAMS_IN_PATH "${CMAKE_CURRENT_SOURCE_DIR}/source/precompile/precompile.json.in" )set (PICCOLO_PRECOMPILE_PARAMS_PATH "${PRECOMPILE_TOOLS_PATH}/precompile.json" )configure_file (${PICCOLO_PRECOMPILE_PARAMS_IN_PATH} ${PICCOLO_PRECOMPILE_PARAMS_PATH} )
其实就是configure_file这个函数将${PICCOLO_PRECOMPILE_PARAMS_IN_PATH}
里的变量展开,放到了${PICCOLO_PRECOMPILE_PARAMS_PATH}
里。${PICCOLO_PRECOMPILE_PARAMS_IN_PATH}
就是precompile.cmake同目录下的precompile.json.in,它的内容很简单,就是@PICCOLO_RUNTIME_HEADS@,@PICCOLO_EDITOR_HEADS@
这两个变量分别在engine/source/runtime/CMakeLists.txt和engine/source/editor/CMakeLists.txt中被设置,并设置到了PARENT_SCOPE,所以只有这两个文件夹下的include文件会作为parser_header.h的内容。回到开头的engine目录下的CMakeLists.txt,
1 2 3 4 5 6 7 add_subdirectory (source/runtime)add_subdirectory (source/editor)add_subdirectory (source/meta_parser)set (CODEGEN_TARGET "PiccoloPreCompile" )include (source/precompile/precompile.cmake)
这也就是为什么先添加runtime和editor为subdirectory,再include
precompile的原因——为了让它们先设置好这些变量。
那么下一个问题,反射文件是什么时候生成的。实际上这点非常简单,source/meta_parser/main.cpp中的parser.generateFiles();
就是生成反射文件。在MetaParser
初始化的时候,它初始化了ReflectionGenerator
和SerializerGenerator
,并在generateFiles
中遍历它们并generate。
最后,我们来看一下parser_header.h到底是用来做什么的。在parse完整个project后,也就是生成parser_header.h后,MetaParser
还做了一些事。
1 2 3 4 5 6 7 8 9 10 m_translation_unit = clang_createTranslationUnitFromSourceFile ( m_index, m_source_include_file_name.c_str (), static_cast <int >(arguments.size ()), arguments.data (), 0 , nullptr ); auto cursor = clang_getTranslationUnitCursor (m_translation_unit); Namespace temp_namespace;buildClassAST (cursor, temp_namespace); temp_namespace.clear ();
它将这个文件传递给llvm的clang工具得到抽象语法树,之后遍历抽象语法树,通过TRY_ADD_LANGUAGE_TYPE
维护两张表m_schema_modules
和m_type_table
,这两张表会在生成反射文件的函数generateFiles
被用到,其中m_schema_modules
中的内容会直接通过参数的形式传递给generate,而m_type_table
的访问则是通过将私有的访问函数bind给generator实现(不了解bind和placeholder的话可以在这篇文章 搜索std::placeholder)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 std::string MetaParser::getIncludeFile (std::string name) { auto iter = m_type_table.find (name); return iter == m_type_table.end () ? std::string () : iter->second; } MetaParser::MetaParser (const std::string project_input_file, const std::string include_file_path, const std::string include_path, const std::string sys_include, const std::string module_name, bool is_show_errors) : m_project_input_file (project_input_file), m_source_include_file_name (include_file_path), m_index (nullptr ), m_translation_unit (nullptr ), m_sys_include (sys_include), m_module_name (module_name), m_is_show_errors (is_show_errors) { m_work_paths = Utils::split (include_path, ";" ); m_generators.emplace_back (new Generator::SerializerGenerator ( m_work_paths[0 ], std::bind (&MetaParser::getIncludeFile, this , std::placeholders::_1))); m_generators.emplace_back (new Generator::ReflectionGenerator ( m_work_paths[0 ], std::bind (&MetaParser::getIncludeFile, this , std::placeholders::_1))); }
至此,我们就对整个项目的框架有了一定的认识,下一篇如果不出意外的话,我将会侧重解读反射系统的实现。