Piccolo源码解读(一)——项目结构

这篇文章基于的Piccolo版本的commit号为214542257eeda28b09495fe38910d42fa2ba48b4

项目的构建官方给出的视频已经有所介绍,这里就不再赘述了,需要注意的是windows系统建议使用MSVC,最好不要尝试使用MinGW的gcc或clang,因为依赖的第三方库JoltPhysics的代码里会根据你是windows而假定你使用MSVC,光是修改Jolt的CMakeLists似乎不太能使它正确通过编译。

Piccolo的编译运行主要分为以下几个部分

  1. 生成PiccoloParser
  2. 使用PiccoloParser读取项目源代码文件,在build目录下生成所有被include的头文件parser_header.h,并基于此生成反射所需的头文件,生成的文件在engine/source/_generated目录下,这部分被称为Precompile
  3. 根据生成好的反射头文件和项目源文件,生成可执行引擎程序

这里我们直接看engine目录下的CMakeLists.txt,前面都是些准备工作,关键是最后这段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add_subdirectory(shader)

add_subdirectory(3rdparty)

add_subdirectory(source/runtime)
add_subdirectory(source/editor)
add_subdirectory(source/meta_parser)
#add_subdirectory(source/test)

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_subdirectoryinclude,添加了各个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
# Called first time when building target 
add_custom_target(${PRECOMPILE_TARGET} ALL

# COMMAND # (DEBUG: DON'T USE )
# this will make configure_file() is called on each compile
# ${CMAKE_COMMAND} -E touch ${PRECOMPILE_PARAM_IN_PATH}a

# If more than one COMMAND is specified they will be executed in order...
COMMAND
${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
### BUILDING ====================================================================================
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/macro.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)
#add_subdirectory(source/test)

set(CODEGEN_TARGET "PiccoloPreCompile")
include(source/precompile/precompile.cmake)

这也就是为什么先添加runtime和editor为subdirectory,再include precompile的原因——为了让它们先设置好这些变量。

那么下一个问题,反射文件是什么时候生成的。实际上这点非常简单,source/meta_parser/main.cpp中的parser.generateFiles();就是生成反射文件。在MetaParser初始化的时候,它初始化了ReflectionGeneratorSerializerGenerator,并在generateFiles中遍历它们并generate。

最后,我们来看一下parser_header.h到底是用来做什么的。在parse完整个project后,也就是生成parser_header.h后,MetaParser还做了一些事。

1
2
3
4
5
6
7
8
9
10
// engine/meta_parser/parser/parser/parser.cpp第175行
m_translation_unit = clang_createTranslationUnitFromSourceFile(
m_index, m_source_include_file_name.c_str(), static_cast<int>(arguments.size()), arguments.data(), 0, nullptr); // m_source_include_file_name就是parser_header.h
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_modulesm_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)));
}

至此,我们就对整个项目的框架有了一定的认识,下一篇如果不出意外的话,我将会侧重解读反射系统的实现。


Piccolo源码解读(一)——项目结构
https://jhex-git.github.io/posts/2317033436/
作者
JointHex
发布于
2023年8月11日
许可协议