返回首页

constexpr C++ 中的 C 编译器

本文描述了在编译时 C++ 中实现 C 子集编译器。它涵盖了绕过 constexpr 限制、签名映射、本地函数绑定以及 lexer/parser 架构。

完全在 constexpr 中的 C 编译器
Advertisement 728x90

在C++编译时实现类C语言编译器

一款C语言子集的编译器已被开发出来,在C++的constexpr上下文中实现。源代码被转换为自定义字节码,在运行时于虚拟机中执行。这种方法确保了零解释器开销和完全的类型安全。

优势包括二进制文件中无编译器代码、无需运行时编译,以及防止函数签名不匹配。缺点:缺乏LLVM级别的优化,无法热重载。

绕过constexpr限制

C++20通过动态内存分配和对std::vector的支持扩展了constexpr,但结果无法直接传递到运行时。需要模板包装器。

Google AdInline article slot

通过模板传递字符串

字符串字面量通过const_string模板类传递:

template<std::size_t N>  
struct const_string {  
  constexpr const_string() = default;  
  
  constexpr const_string(const char (&str)[N]) {
    std::copy_n(str, N, value);  
  }  
  
  constexpr operator std::string_view() const {  
    return {value, value + N - 1};  
  }  
  
  char value[N]{};  
  const std::size_t length = N;  
};

// 用法:
template<const_string str>
auto very_smart_function(...) { /* ... */ }

从constexpr提取数组

将std::vector转换为std::array需要通过lambda函数在编译时固定大小:

template<auto data_getter>
constexpr auto to_array() {  
  using value_type = typename decltype(data_getter())::value_type;  
  constexpr static std::size_t size = data_getter().size();  
  
  std::array<value_type, size> out;  
  auto in = data_getter();  
  for (std::size_t i = 0; i < size; ++i) {  
    out[i] = in[i];  
  }  
  return out;
}

template<const_string str>
constexpr auto lex() {
  constexpr static auto data_getter = [] constexpr {
    return lexer{static_cast<std::string_view>(str)}.lex();
  };
  return to_array<data_getter>();
}

处理编译错误

在C++26之前,static_assert需要字符串字面量。解决方案是使用ErrorMessage模板,错误文本在参数中:

Google AdInline article slot
template<const_string Msg>  
struct ErrorMessage {  
  static_assert(false, "检查模板参数以获取详细信息");  
};

template<auto err_getter>  
consteval auto report_error() -> void {
#ifdef KORKA_FEATURE_FORMATTED_STATIC_ASSERT  
  static_assert(false, to_string(err_getter()));  
#else  
  constexpr auto msg = const_string_from_string_view<[] { return to_string(err_getter()); }>();  
  std::ignore = ErrorMessage<msg>{};    
#endif
}

C++26中的输出:词法分析器错误:第12行未终止的字符串

映射函数签名

为了匹配字符串名称与签名类型,使用了哈希和重载:

template<auto function_info_getter, std::size_t... Is>
struct signature_mapper<function_info_getter, std::index_sequence<Is...>> {
  consteval static auto hash(auto &&v) -> std::size_t {
    return frozen::elsa<std::string_view>{}(v, 0);
  }
  
  constexpr static auto _overloaded = overloaded{
    ([] (unique_type<hash(function_info_getter(Is).name)>)
     -> const_function_info_to_signature_t<[] { return function_info_getter(Is); }> * {
      return nullptr;
    })...
  };
  
  template<const_string name>
  using get_signature_t = std::remove_pointer_t<decltype(_overloaded(unique_type<hash(name)>{}))>;
};

提取:compile_result.function<"fib">();

Google AdInline article slot

原生函数绑定

通过wrapped_function将C++函数与脚本语言链接:

consteval auto wrap(std::string_view name) {  
  return wrapped_function<std::decay_t<decltype(func)>>{  
    binding_wrapper<func>,  
    name  
  };  
}

constexpr auto bindings = korka::make_bindings(  
  korka::wrap<fib>("cpp_fib"),  
  korka::wrap<print_n>("print_n")  
);

binding_wrapper生成代码以从虚拟机提取参数并返回结果。

编译器架构

分为三个阶段:

  • 词法分析器:在字符循环中进行标记化
  • 解析器:AST构建
  • 字节码生成器:带有语义分析的发射

示例词法分析器:

constexpr auto scan_token() -> std::optional<std::expected<lex_token, error_t>> {  
  char c = advance();  
  switch (c) {  
    case '{': return make_token(lex_kind::kOpenBrace);  
    case '}': return make_token(lex_kind::kCloseBrace);  
    case '(': return make_token(lex_kind::kOpenParenthesis);  
    case ')': /* ... */
    case ' ': case '\r': case '\t': return std::nullopt;
  }
}

关键要点

  • 编译器完全为constexpr,字节码嵌入二进制文件中
  • 通过const_string和lambda包装器绕过C++20限制
  • 使用名称哈希进行类型安全的函数提取
  • 带有自动虚拟机代码生成的C++函数绑定
  • 无优化的基本词法分析器/解析器

— Editorial Team

Advertisement 728x90

继续阅读