Zurück zur Startseite

C-Compiler in constexpr C++

Der Artikel beschreibt die Implementierung eines C-Subset-Compilers in C++ zur Kompilierzeit. Er behandelt die Umgehung von constexpr-Beschränkungen, Signatur-Mapping, native Funktionsbindings und Lexer-/Parser-Architektur.

C-Compiler vollständig in constexpr
Advertisement 728x90

Ein C-ähnlicher Sprachcompiler zur Compile-Zeit in C++

Ein Compiler für eine Teilmenge der C-Sprache wurde entwickelt, der in C++ innerhalb eines constexpr-Kontexts implementiert ist. Quellcode wird in benutzerdefinierten Bytecode umgewandelt, der zur Laufzeit in einer virtuellen Maschine ausgeführt wird. Dieser Ansatz gewährleistet keinen Interpreter-Overhead und volle Typsicherheit.

Vorteile sind: Kein Compiler-Code im Binärformat, keine Laufzeitkompilierung und Schutz vor Funktionssignatur-Fehlern. Nachteile: Fehlen von LLVM-Optimierungen und Unfähigkeit zum Hot-Reload.

Umgang mit constexpr-Einschränkungen

C++20 erweitert constexpr um dynamische Speicherzuweisung und Unterstützung für std::vector, aber Ergebnisse können nicht direkt zur Laufzeit übertragen werden. Template-Wrapper sind erforderlich.

Google AdInline article slot

Übergabe von Strings durch Templates

String-Literale werden über die const_string-Template-Klasse übergeben:

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;  
};

// Verwendung:
template<const_string str>
auto sehr_schlanke_funktion(...) { /* ... */ }

Extrahieren von Arrays aus constexpr

Die Umwandlung von std::vector in std::array erfordert das Festlegen der Größe zur Compile-Zeit über eine Lambda-Funktion:

template<auto data_getter>
constexpr auto zu_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 zu_array<data_getter>();
}

Behandlung von Kompilierungsfehlern

Bis C++26 erfordert static_assert String-Literale. Die Lösung ist ein ErrorMessage-Template mit dem Fehlertext im Parameter:

Google AdInline article slot
template<const_string Msg>  
struct ErrorMessage {  
  static_assert(false, "Details im Template-Parameter prüfen");  
};

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

Ausgabe in C++26: Lexer-Fehler: Unbeendeter String in Zeile 12.

Zuordnung von Funktionssignaturen

Um String-Namen mit Signaturtypen abzugleichen, werden Hashing und Überladung verwendet:

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 _überladen = überladen{
    ([] (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(_überladen(unique_type<hash(name)>{}))>;
};

Extraktion: compile_result.function<"fib">();.

Google AdInline article slot

Native Funktionsbindungen

Verknüpfung von C++-Funktionen mit der Skriptsprache über wrapped_function:

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

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

binding_wrapper generiert Code, um Argumente aus der VM zu extrahieren und Ergebnisse zurückzugeben.

Compiler-Architektur

Aufgeteilt in drei Phasen:

  • Lexer: Tokenisierung in einer Zeichenschleife
  • Parser: AST-Konstruktion
  • Bytecode-Generator: Emission mit semantischer Analyse

Beispiel-Lexer:

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;
  }
}

Wichtige Punkte

  • Der Compiler ist vollständig constexpr, mit Bytecode im Binärformat eingebettet
  • Workarounds für C++20-Einschränkungen über const_string und Lambda-Wrapper
  • Typsichere Funktionsextraktion mit Namens-Hashing
  • C++-Funktionsbindungen mit automatischer VM-Code-Generierung
  • Grundlegender Lexer/Parser ohne Optimierungen

— Editorial Team

Advertisement 728x90

Weiterlesen