基于 LibTooling 生成 C++ 的 Objective-C++ 包装接口想法初步验证

基于 LibTooling 生成 C++ 的 Objective-C++ 包装接口想法初步验证

产生这个想法,源于一位大佬问我 Swift 调用 C++ 的解决方案是啥?

已知方案

  • C 包装 C++
  • Objective-C++ 包装 C++

上面两种方式都需收开发人员手动去写对应的包装接口,作为一个新生代的农民工,应该想办法自动的完成这件事,为自己挤出更多的摸鱼划水的时间。

如何自动生成?

  • 之前在研究一个动态二维码code时,一开始是用Rust实现了一遍,然后通过Rust生成 ffi接口,以供iOS Android使用。Rust 通过在函数使用 extern "C" #[no_mangle]。然后通过 cbindgen 这类工具即可自动生成对应的 ffi 接口
  • 所以我就想,能不能用类似的方式,在C++的类,函数上加点料,然后再通过一个工具,找出这些,自动生成对应的 Objective-C++
    • 答案是可以的, 可以利用 __attribute__((annotate()))
    • 这个料又如何读出来呢?

AST

  • 示例
#ifndef annotate_h
#define annotate_h

#define GEN_ANNOTATE_CLASS __attribute__((annotate("native_gen_class")))
#define GEN_ANNOTATE_METHOD __attribute__((annotate("native_gen_method")))
#define GEN_ANNOTATE_INCLUDE __attribute__((annotate("native_gen_class_include")))
#endif /* annotate_h */
#ifndef kit_hpp
#define kit_hpp

#include <string>

#include "annotate.h"

using namespace std;

namespace TT {

struct GEN_ANNOTATE_CLASS GEN_ANNOTATE_INCLUDE Kit {
    int a = 0;
    GEN_ANNOTATE_METHOD bool vaild() const;
};
};  // namespace TT

class GEN_ANNOTATE_CLASS Kit2 {
    int a = 0;
    bool vaild() const;

    GEN_ANNOTATE_METHOD void test(string str);
    
    GEN_ANNOTATE_METHOD void test2(const char *str);
};

#endif /* kit_hpp */
  • 打印AST
clang++ -Xclang -ast-dump -fsyntax-only kit.cpp

  • 可以看到图中的 AnnotateAttr `就是我们的加料

LibTooling

通过LibTooling解析源码AST,只要找到符合的AST我们就生成对应的Objective-C++源码, 具体怎么做呢?

mkdir LLVM
cd LLVM
wget https://github.com/llvm/llvm-project/archive/refs/tags/llvmorg-13.0.0-rc1.zip
unzip llvmorg-13.0.0-rc1.zip
  • 创建工程

    • 在解压后的源码目录clang/examples目录下新建一个文件夹,如CppNativeGen
    • clang/examples/CMakeLists.txt 追加一行 add_subdirectory(CppNativeGen)
    • CppNativeGen 目录结构如下
    CppNativeGen
    ├── CMakeLists.txt
    └── CppNativeGen.cpp
    
    • CppNativeGen/MakeLists.txt 内容如下
    
    add_clang_executable(cpp-native-gen
        CppNativeGen.cpp
    )
    
    target_link_libraries(cpp-native-gen PRIVATE
        clangAST
        clangBasic
        clangDriver
        clangLex
        clangParse
        clangSema
        clangFrontend
        clangSerialization
        clangTooling
        clangToolingCore
    )
    
  • 生成Xcode工程

    • 辅助脚本 build.sh
    #!/usr/bin/env bash
    
    set -e
    set -o pipefail
    set -u
    
    
    ROOT_DIR=`pwd`
    SOURCE_CODE_DIR=$ROOT_DIR/llvm-project-llvmorg-13.0.0-rc1
    BUILD_DIR=$ROOT_DIR/build_dir
    
    XCODE_BUILD_DIR=$BUILD_DIR/build_xcode
    XCODE_OUT_DIR=$BUILD_DIR/build_xcode_out
    
    function generate_xcode_project() {
        cd $XCODE_BUILD_DIR
        cmake -G "Xcode" \
        -DCMAKE_OSX_DEPLOYMENT_TARGET="10.14" \
        -DCMAKE_OSX_SYSROOT="macosx" \
        -DCMAKE_OSX_ARCHITECTURES='x86_64;arm64' \
        -DLLVM_TARGETS_TO_BUILD="ARM;X86" \
        -DCMAKE_BUILD_TYPE=Debug \
        -DCMAKE_INSTALL_PREFIX=$XCODE_OUT_DIR \
        -DLLVM_ENABLE_PROJECTS="clang" \
        $SOURCE_CODE_DIR/llvm
    }
    
    function clear_build() {
        echo "clear build dir"
        rm -rf $BUILD_DIR
    }
    
    
    function onCtrlC () {
        clear_build
        exit 0
    }
    
    trap 'onCtrlC' INT
    
    mkdir -p $XCODE_BUILD_DIR
    generate_xcode_project
    
    • 生成工程
    chmod +x ./build.sh # 只需要执行一次
    ./build.sh
    
  • 打开build_dir/build_xcode/LLVM.xcodeproj

    • 打开后会提示创建Scheme, 选择手动, 将 cpp-native-gen 加上即可
  • CppNativeGen.cpp 源码

//===- CppNativeGen.cpp ---------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// Example clang plugin which simply prints the names of all the top-level decls
// in the input file.
//
//===----------------------------------------------------------------------===//

#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/Sema/Sema.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Signals.h"
#include "llvm/Support/raw_ostream.h"

using namespace clang;

namespace {

static llvm::StringRef AnnotationClassName{"native_gen_class"};
static llvm::StringRef AnnotationMethodName{"native_gen_method"};

class CppNativeGenConsumer : public ASTConsumer {
    CompilerInstance &Instance;
    std::set<std::string> ParsedTemplates;

  public:
    CppNativeGenConsumer(CompilerInstance &Instance,
                         std::set<std::string> ParsedTemplates)
        : Instance(Instance)
        , ParsedTemplates(ParsedTemplates) {}

    bool HandleTopLevelDecl(DeclGroupRef DG) override {
        for (DeclGroupRef::iterator i = DG.begin(), e = DG.end(); i != e; ++i) {
            const Decl *D = *i;
            // TODO: NamespaceDecl CXXRecordDecl 需要考虑 嵌套
            if (const NamespaceDecl *NSD = dyn_cast<NamespaceDecl>(D)) {
                for (auto I : NSD->decls()) {
                    if (const CXXRecordDecl *RD = dyn_cast<CXXRecordDecl>(I)) {
                        HandleCXXRecordDecl(RD);
                    }
                }
            } else if (const CXXRecordDecl *RD = dyn_cast<CXXRecordDecl>(D)) {
                HandleCXXRecordDecl(RD);
            }
        }

        return true;
    }

    void HandleCXXRecordDecl(const CXXRecordDecl *RD) {
        if (RD->hasAttr<AnnotateAttr>()) {
            AnnotateAttr *attr = RD->getAttr<AnnotateAttr>();
            if (attr->getAnnotation() == AnnotationClassName) {
                llvm::errs() << (RD->isStruct() ? "annotation struct: " : "annotation class: ") << RD->getNameAsString() << "\n";
                for (auto I : RD->decls()) {
                    if (const CXXMethodDecl *MD = dyn_cast<CXXMethodDecl>(I)) {
                        HandleCXXMethodDecl(MD);
                    }
                }
            }
        }
    }

    void HandleCXXMethodDecl(const CXXMethodDecl *MD) {
//        const CXXRecordDecl *RD = MD->getParent();
        if (MD->hasAttr<AnnotateAttr>()) {
            AnnotateAttr *attr = MD->getAttr<AnnotateAttr>();
            if (attr->getAnnotation() == AnnotationMethodName) {
                llvm::errs() << "\tmethod: " << MD->getNameAsString() << "\n";
                auto numParams = MD->getNumParams();
                for (unsigned int i = 0; i < numParams; i++) {
                    auto PD = MD->getParamDecl(i);
                    //                    PD->dump();

                    QualType type = PD->getType();

                    //                    if (true && !type.isNull()) {
                    //                      // If the type is sugared, also dump a (shallow) desugared type.
                    //                      SplitQualType D_split = type.getSplitDesugaredType();
                    //                      if (T_split != D_split)
                    //                          llvm::errs() << ":'" << QualType::getAsString(D_split, PrintPolicy) << "'";
                    //                    }

                    llvm::errs() << "\t\tparam: name -> " << PD->getNameAsString() << " type -> " << QualType::getAsString(type.split(), PrintingPolicy{{}}) << "\n";
                }
            }
        }
    }
};

class CppNativeGenAction : public PluginASTAction {
    std::set<std::string> ParsedTemplates;

  protected:
    std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                   llvm::StringRef) override {
        // 忽略警告
        CI.getDiagnostics().setClient(new IgnoringDiagConsumer());
        return std::make_unique<CppNativeGenConsumer>(CI, ParsedTemplates);
    }

    bool ParseArgs(const CompilerInstance &CI,
                   const std::vector<std::string> &args) override {

        return true;
    }
    void PrintHelp(llvm::raw_ostream &ros) {
        ros << "Help for CppNativeGen plugin goes here\n";
    }
};

}  // namespace

// static FrontendPluginRegistry::Add<CppNativeGenAction>
//     X("cpp-native-gen", "print function names");

using namespace clang;
using namespace tooling;
using namespace llvm;

static llvm::cl::OptionCategory OptsCategory("cpp-native-gen");
int main(int argc, const char **argv) {
    //    sys::PrintStackTraceOnErrorSignal(argv[0], false);
    //    PrettyStackTraceProgram X(argc, argv);

    auto ExpectedParser =
        CommonOptionsParser::create(argc, argv, OptsCategory);
    if (!ExpectedParser) {
        llvm::errs() << ExpectedParser.takeError();
        return 1;
    }
    CommonOptionsParser &OptionsParser = ExpectedParser.get();
    ClangTool Tool(OptionsParser.getCompilations(),
                   OptionsParser.getSourcePathList());

//    auto cmd = OptionsParser.getCompilations().getCompileCommands(OptionsParser.getSourcePathList().front()).front();

    /*
     /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/xxx
     
     /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/clang 是对上面路径的软连
    */
    Tool.appendArgumentsAdjuster(getInsertArgumentAdjuster(
        "-I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/clang/include",
        ArgumentInsertPosition::END));

    Tool.appendArgumentsAdjuster(getInsertArgumentAdjuster(
        "-I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1",
        ArgumentInsertPosition::END));

    Tool.appendArgumentsAdjuster(getInsertArgumentAdjuster(
        "-I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include",
        ArgumentInsertPosition::END));

    return Tool.run(clang::tooling::newFrontendActionFactory<CppNativeGenAction>().get());
}
  • cpp-native-gen Edit Scheme 添加源码文件
  • 运行即可看到控制台输出如下

剩下的就是,利用上面步骤收集的信息然后生成对应的Objective-C++源码,当然其中还是有很多细节的,比如参数类型的转换

Edit Edit this page