如何通过CMake与Clang在预编译后编译前插入脚本处理带[[log]]属性的函数?
Great question! Let's break down how to implement your custom preprocessing workflow with CMake, including both a general approach and a more robust Clang-specific alternative.
Core Workflow Overview
First, let's align on the exact pipeline you want:
- Run standard preprocessing on source files
- Apply custom script to modify preprocessed output (wrap
[[log]]-marked functions with a logger that records params/return values)- Compile & link the modified files as usual
Here's how to make this happen with CMake, plus optimized options using Clang.
Approach 1: General CMake Implementation (Works with Any Compiler)
This method explicitly splits preprocessing, custom modification, and compilation into separate steps in your CMake script.
Step 1: CMake Script Setup
Add this to your CMakeLists.txt to intercept preprocessed files and run your custom script:
cmake_minimum_required(VERSION 3.18) project(LogWrappedApp) # Path to your custom processing script (e.g., Python, Bash) set(CUSTOM_PROCESSOR "${CMAKE_SOURCE_DIR}/wrap_log_functions.py") # Collect all source files file(GLOB_RECURSE SOURCES "src/*.cpp") set(PROCESSED_SOURCES "") foreach(SOURCE_FILE ${SOURCES}) # Generate paths for intermediate files get_filename_component(BASE_NAME ${SOURCE_FILE} NAME_WE) set(PREPROCESSED_FILE "${CMAKE_BINARY_DIR}/preprocessed/${BASE_NAME}.i") set(MODIFIED_FILE "${CMAKE_BINARY_DIR}/processed/${BASE_NAME}_modified.i") # 1. Run standard preprocessing to generate .i file add_custom_command( OUTPUT ${PREPROCESSED_FILE} COMMAND ${CMAKE_CXX_COMPILER} -E ${CMAKE_CXX_FLAGS} ${SOURCE_FILE} -o ${PREPROCESSED_FILE} DEPENDS ${SOURCE_FILE} COMMENT "Preprocessing ${BASE_NAME}.cpp..." ) # 2. Run your custom script to modify the preprocessed output add_custom_command( OUTPUT ${MODIFIED_FILE} COMMAND python3 ${CUSTOM_PROCESSOR} ${PREPROCESSED_FILE} ${MODIFIED_FILE} DEPENDS ${PREPROCESSED_FILE} ${CUSTOM_PROCESSOR} COMMENT "Wrapping log functions in ${BASE_NAME}.i..." ) # Track modified files for compilation list(APPEND PROCESSED_SOURCES ${MODIFIED_FILE}) endforeach() # Compile modified preprocessed files into your executable add_executable(my_app ${PROCESSED_SOURCES}) # Tell compiler to treat .i files as preprocessed C++ code (skip re-preprocessing) target_compile_options(my_app PRIVATE -x c++) # Ensure logger macros are available (add your log header path here) target_include_directories(my_app PRIVATE "${CMAKE_SOURCE_DIR}/include")
Step 2: Custom Processing Script Example
Here's a Python script (wrap_log_functions.py) that wraps [[log]] functions with logger calls. It uses regex to match function signatures (adjust regex for your code style):
import re import sys def wrap_log_functions(input_path, output_path): with open(input_path, 'r') as f: content = f.read() # Match functions marked with [[log]] (adjust regex for complex signatures) log_func_pattern = re.compile( r'\[\[log\]\]\s*(?P<ret_type>\w+)\s*(?P<func_name>\w+)\s*\((?P<params>.*?)\)\s*{(?P<body>.*?)}', re.DOTALL ) def inject_logging(match): ret_type = match.group('ret_type') func_name = match.group('func_name') params = match.group('params') body = match.group('body').strip() # Extract parameter names for logging param_names = [p.split()[-1] for p in params.split(',') if p.strip()] if params else [] param_log = ', '.join(param_names) # Generate wrapped function body if ret_type == 'void': wrapped_body = f''' LOG_ENTER("{func_name}", {param_log}); {body}; LOG_EXIT("{func_name}"); ''' else: wrapped_body = f''' LOG_ENTER("{func_name}", {param_log}); {ret_type} result = {body}; LOG_EXIT("{func_name}", result); return result; ''' return f'{ret_type} {func_name}({params}) {{{wrapped_body}}}' # Apply replacements and save output processed_content = log_func_pattern.sub(inject_logging, content) with open(output_path, 'w') as f: f.write(processed_content) if __name__ == '__main__': if len(sys.argv) != 3: print("Usage: python wrap_log_functions.py <input_preprocessed_file> <output_modified_file>") sys.exit(1) wrap_log_functions(sys.argv[1], sys.argv[2])
Step 3: Define Logger Macros
Add a header file (include/log_macros.h) to define your logging functionality:
#ifndef LOG_MACROS_H #define LOG_MACROS_H #include <iostream> #define LOG_ENTER(func_name, ...) \ std::cout << "[LOG] Entering " << func_name << ": " << #__VA_ARGS__ << std::endl; #define LOG_EXIT(func_name, ...) \ std::cout << "[LOG] Exiting " << func_name << ": " << #__VA_ARGS__ << std::endl; #endif // LOG_MACROS_H
Approach 2: CMake + Clang (Robust AST-Based Processing)
For more reliable code modification (avoiding regex pitfalls with complex C++ syntax), use Clang's AST matchers to directly parse and modify source code before preprocessing.
Step 1: Build a Custom Clang Tool
Write a C++ tool using Clang Tooling to find [[log]] functions and inject logging:
#include "clang/AST/ASTConsumer.h" #include "clang/ASTMatchers/ASTMatchers.h" #include "clang/Frontend/FrontendActions.h" #include "clang/Tooling/Tooling.h" #include "llvm/Support/CommandLine.h" using namespace clang; using namespace clang::ast_matchers; using namespace clang::tooling; static llvm::cl::OptionCategory LogWrapperToolCategory("Log Wrapper Tool Options"); class LogWrapperCallback : public MatchFinder::MatchCallback { public: void run(const MatchFinder::MatchResult& Result) override { if (const FunctionDecl* FD = Result.Nodes.getNodeAs<FunctionDecl>("logFunc")) { // Skip functions without [[log]] attribute or body if (!FD->hasAttr<AnnotateAttr>("log") || !FD->getBody()) return; ASTContext* Ctx = Result.Context; SourceManager& SM = Ctx->getSourceManager(); Replacements& Replaces = *Result.SourceManager; // Insert enter-log at start of function body std::string enterLog = "LOG_ENTER(\"" + FD->getNameAsString() + "\""; for (const ParmVarDecl* Param : FD->parameters()) { enterLog += ", " + Param->getNameAsString(); } enterLog += ");\n"; Replaces.insert(Replacement(SM, FD->getBody()->getBeginLoc(), enterLog)); // Insert exit-log (handle void return type) if (!FD->getReturnType()->isVoidType()) { // Wrap body in result variable and exit log std::string bodyText = Lexer::getSourceText( CharSourceRange::getTokenRange(FD->getBody()->getSourceRange()), SM, Ctx->getLangOpts() ).str(); std::string wrappedBody = FD->getReturnType().getAsString() + " result = " + bodyText + ";\n" "LOG_EXIT(\"" + FD->getNameAsString() + "\", result);\nreturn result;"; Replaces.insert(Replacement(SM, FD->getBody()->getSourceRange(), wrappedBody)); } else { std::string exitLog = "LOG_EXIT(\"" + FD->getNameAsString() + "\");\n"; Replaces.insert(Replacement(SM, FD->getBody()->getEndLoc().getLocWithOffset(-1), exitLog)); } } } }; class LogWrapperAction : public ASTFrontendAction { public: std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance& CI, StringRef) override { auto finder = std::make_unique<MatchFinder>(); finder->addMatcher(functionDecl(hasAttr("log")).bind("logFunc"), &callback); return finder->newASTConsumer(); } private: LogWrapperCallback callback; }; int main(int argc, const char** argv) { auto parser = CommonOptionsParser::create(argc, argv, LogWrapperToolCategory); if (!parser) { llvm::errs() << parser.takeError(); return 1; } ClangTool tool(parser->getCompilations(), parser->getSourcePathList()); return tool.run(newFrontendActionFactory<LogWrapperAction>().get()); }
Step 2: Integrate Clang Tool into CMake
Add this to your CMakeLists.txt to compile the tool and use it to modify source files:
# Build the Clang Log Wrapper Tool add_executable(LogWrapperTool src/log_wrapper_tool.cpp) find_package(LLVM REQUIRED CONFIG) find_package(Clang REQUIRED CONFIG) target_link_libraries(LogWrapperTool PRIVATE clangTooling clangASTMatchers) # Process source files with the Clang tool set(MODIFIED_SOURCES "") foreach(SOURCE_FILE ${SOURCES}) get_filename_component(BASE_NAME ${SOURCE_FILE} NAME_WE) set(MODIFIED_FILE "${CMAKE_BINARY_DIR}/modified/${BASE_NAME}_modified.cpp") add_custom_command( OUTPUT ${MODIFIED_FILE} COMMAND LogWrapperTool ${SOURCE_FILE} -o ${MODIFIED_FILE} DEPENDS ${SOURCE_FILE} LogWrapperTool COMMENT "Injecting logging into ${BASE_NAME}.cpp..." ) list(APPEND MODIFIED_SOURCES ${MODIFIED_FILE}) endforeach() # Compile modified files into your app add_executable(my_app ${MODIFIED_SOURCES}) target_include_directories(my_app PRIVATE "${CMAKE_SOURCE_DIR}/include") target_compile_features(my_app PRIVATE cxx_std_17)
Key Notes
- Incremental Builds: CMake automatically handles dependencies, so only modified source files/scripts will re-run through the pipeline.
- Preprocessing Flags: Ensure all necessary
-Iinclude paths and-Dmacros are passed to the preprocessing step in the general approach. - Logger Customization: Extend the
LOG_ENTER/LOG_EXITmacros to write to files, use a logging library, or add timestamps as needed.
内容的提问来源于stack exchange,提问作者Michael Litvin

