1 Punkte von GN⁺ 4 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Rust-Binärdateien durchlaufen vor fn main() eine Laufzeit-Initialisierungsphase, in der unter anderem Panic-/Unwinding-Behandlung und die Umwandlung von Programmargumenten erfolgen
  • Wenn der Loader des Betriebssystems die Kontrolle an den Entry Point übergibt, führen die C-Laufzeit und die Rust-Laufzeit Initialisierungsfunktionen aus; mit #[unsafe(link_section = "...")] und dem Konstruktor-Ansatz lässt sich Pre-Main-Code platzieren
  • Linker-Sektionen sammeln Daten, die von mehreren Crates beigesteuert werden, beim Erstellen der Binärdatei an einer Stelle; link-section macht sie wie Rust-Slices behandelbar
  • Mit ctor und link-section zusammen lassen sich Muster wie die Registrierung von CLI-Subcommands oder das Sortieren eines String-Interning-Pools schon vor main aufbauen und danach ohne Locks lesen
  • Dieser Ansatz bietet allokationsfreie Aggregation und Inversion of Control, aber wegen erschwerter Dead-Code-Eliminierung, Einschränkungen bei Konstruktoren, Plattformunterschieden und Grenzen bei der Miri-Kompatibilität sollte sein Einsatz sorgfältig gewählt werden

Die Phase vor main in einer Rust-Binärdatei

  • Jede Rust-Binärdatei hat ein fn main(), aber der tatsächliche Ausführungsfluss erreicht main erst nach dem Durchlaufen des Betriebssystem-Loaders und der Laufzeit-Initialisierung
  • In C gibt es die als libc bekannte C-Laufzeit, und Rust besitzt über die Standardbibliothek eine eigene Laufzeit, die auf der C-Laufzeit aufsetzt und darüber höherwertige Abstraktionen bereitstellt
  • Der Zweck der Laufzeit besteht darin, den Code der Entwickler mit dem Betriebssystem der Plattform zu integrieren
  • Die C-Laufzeit richtet in der Phase vor main Laufzeitdienste wie Allokation, Dateizugriff und Thread-Local Storage ein
  • Rust bereitet zu diesem Zeitpunkt Panic- und Unwinding-Behandlung vor und wandelt C-artige Programmargumente in die Schnittstelle std::env::args um
  • Die Pre-Main-Phase läuft vor dem Benutzercode, ist Single-Threaded und findet in einer Umgebung mit vorhersagbarer Reihenfolge statt, was sie für deterministische Initialisierung geeignet macht

Entry Point

  • Eine Binärdatei startet, wenn der Loader des Betriebssystems sie in den Speicher lädt, die Umgebung einrichtet und dann die Kontrolle übergibt
  • Unter Linux wird der Entry Point im Feld e_entry des ELF-Headers gespeichert; standardmäßig platziert der Linker dort die Adresse eines Symbols namens _start
  • Auch Windows hat einen ähnlichen Hook; eine ausführbare Datei startet in der Funktion _WinMainCRTStartup
  • Frühes Runtime-Bootstrapping war ein statischer Aufrufbaum von Funktionen wie der Initialisierung von Datei-I/O oder des Allokators
  • Mit zunehmender Komplexität der Laufzeit wuchs auch dieser statische Initialisierungs-Aufrufbaum, und Binärdateien enthielten mehr Funktionen der C-Laufzeit, die womöglich gebraucht wurden oder auch nicht
  • Als Linker ungenutzten Code schon vor dem Erstellen der Binärdatei entfernen konnten, wurde ein Weg benötigt, den statischen Initialisierungs-Aufrufbaum zu ersetzen
  • Der GCC-Ansatz __attribute__((constructor)) legte eine Liste von Funktionszeigern für Initialisierungsfunktionen in einem zusammenhängenden Bereich der Binärdatei ab, den die C-Laufzeit beim Start durchlief und aufrief
  • Konstruktoren konnten Prioritäten erhalten; so kann zum Beispiel malloc vor gepufferter Datei-I/O initialisiert werden müssen
  • Die moderne glibc-Laufzeit unter Linux speichert Funktionszeiger in .init_array, und mit einem numerischen Suffix lässt sich die Ausführungsreihenfolge festlegen
  • Prioritätswerte von 100 oder darunter sind für die Laufzeit selbst reserviert, daher sollte Code mit C-Laufzeit 101 oder höher verwenden
  • In Rust lassen sich mit Attributen wie #[used] und #[unsafe(link_section = ".init_array.101")] Zeiger auf Initialisierungsfunktionen platzieren

linktime: ctor, link-section usw.

  • Die Beispiele funktionieren unter Linux und mehreren BSDs, sind aber nicht als plattformübergreifende Beispiele konzipiert
  • macOS unterstützt start- und stop-Symbole, allerdings unter anderen Namen; Windows unterstützt keine start-/stop-Symbole, besitzt aber praktisch äquivalente Regeln zur Sektionsanordnung
  • ctor und link-section sind Crates aus dem Projekt linktime, das plattformspezifische Unterschiede und die Komplexität der Linker-Arbeit abstrahiert
  • inventory und linkme sind weit verbreitete Crates, die auf demselben Prinzip aufbauen, haben für die Beispiele hier aber Einschränkungen
  • Die Crate ctor übernimmt den Boilerplate-Code, um Konstruktoren plattformübergreifend zu registrieren
  • Funktionen mit einem Attribut wie #[ctor(unsafe, priority = 101)] werden von der C-Laufzeit aufgerufen, nachdem der Linker alles angeordnet hat, auch wenn sie im Code nie direkt aufgerufen werden

Sektionen und Linker-Skripte

  • Compiler können Daten oder Code an bestimmten Stellen in einer Binärdatei platzieren, auf den meisten Plattformen in Bereichen, die Sektionen genannt werden
  • Rust kann dieselbe Organisationsmöglichkeit über das Attribut link_section nutzen
  • Viele Linker erlauben Entwicklern, Linker-Skripte bereitzustellen; diese Textdateien sagen dem Linker, wie Objektdateien zusammengesetzt werden sollen
  • Mit Linker-Skripten kann eine einzelne C-Datei zu einer Linux-Executable werden oder zu einem rohen Assembler-Block, der in einem Bootsektor einer Festplatte liegt
  • Linker-Skripte können virtuelle Symbole definieren, die nicht in den Quelldateien existieren, aber in C-Code verwendet werden können, um auf Basisdatenzeiger der geladenen Binärdatei zuzugreifen
  • Im Beispiel-Linker-Skript sind _TEXT_START_ und _TEXT_END_ so definiert, dass sie auf Anfang und Ende der Sektion .text zeigen
  • Der Punkt in _TEXT_START_ = .; steht für den Location Counter, der als ein Wert nahe der aktuellen Ausgabeadresse der Binärdatei interpretiert wird

Linker-Symbole

  • Der Linker setzt den Wert von Start-/End-Symbolen nicht auf einen Zeiger, sondern auf die Adresse, an der ein static mit demselben Namen liegen würde
  • Start-/End-Symbole sind keine Zeiger vom Typ *const Type, sondern tragen nur über ihre Adresse Bedeutung und besitzen keine eigenen Daten
  • Eine Sektion besteht aus Daten im Bereich einschließlich des Startsymbols und ausschließlich des Endsymbols
  • Viele Linker können die Grenzen aller Sektionen einer ausführbaren Datei automatisch definieren
  • In der GNU-Toolchain werden für eine Sektion namens MY_SECTION automatisch die Symbole __start_MY_SECTION und __stop_MY_SECTION definiert
  • macOS hat ein ähnliches Muster und erzeugt für jede Sektion die Symbole section$start und section$end
  • Beim GNU-Linker heißen Sektionen, die im Linker-Skript nicht ausdrücklich genannt werden, Orphan Sections
  • Der Linker definiert Präfix-Symbole wie _start und _stop nur dann automatisch, wenn der Sektionsname mit einem C-Symbolnamen kompatibel ist
  • our_strings funktioniert, our.strings oder .our_strings dagegen nicht auf dieselbe Weise
  • Da Grenzsymbole keine Daten haben und nur ihre Adresse wichtig ist, werden sie im Beispiel als MaybeUninit<()> dargestellt
  • Ein idealer „opaker externer Typ“ ist in Stable Rust noch nicht vorhanden, daher dient MaybeUninit als Ersatz
  • Einen Zeiger mit &raw const auf ein static-Element zu bilden ist immer gültig, daher kann man die Adresse sicher erhalten, ohne den Wert zu lesen
  • link-section abstrahiert diese Details von Linker-Sektionen und wandelt sie in Rust-Slices um, auf denen sich Standard-Slice-Operationen verwenden lassen
  • Die Stärke von Link-Sektionen liegt darin, dass jede Crate, die Code für die Binärdatei bereitstellt, Elemente in dieselbe Sektion eintragen kann, die der Linker kurz vor dem finalen Erstellen der Binärdatei zusammenführt

Dependency Injection

  • Das sektionsbasierte Registrierungsmodell funktioniert nach demselben Prinzip wie Dependency Injection
  • Frameworks wie Dagger und Spring basieren ebenfalls auf dem Prinzip, dass der Verbraucher registrierter Daten nicht an den Anbieter gekoppelt sein sollte
  • Anbieter registrieren Daten dort, wo sie definiert werden, und Verbraucher lesen die Registry aus
  • Bei klassischer Dependency Injection muss das Framework beim Start häufig den Modulgraphen durchlaufen oder geladene Klassen scannen, um Anbieter und Verbraucher zu finden
  • Bei Linker-Sektionen sammelt der Linker die Daten der Anbieter beim Erstellen der Binärdatei und macht sie für Verbraucher leicht lesbar
  • Das Beispiel zur Registrierung von CLI-Subcommands ist ein Fall dieses Musters, bei dem link_section::section zur Registrierung von Subcommands verwendet wird
  • Turbopack nutzt dieses Muster für String-Pool-Konstanten, Registrierungsmechanismen für Serialisierung/Deserialisierung und die Registrierung von turbotask-Inkrementalkompilierungsfunktionen
  • Auch ein hypothetischer Webserver könnte dieses Muster verwenden, um Routen und Middleware bereits beim Build automatisch einzusammeln

Sektionen für Registrierung nutzen

  • Ein Vorteil von Arbeit vor main ist, dass keine Threads laufen, solange man sie nicht ausdrücklich startet
  • In dieser Umgebung lässt sich die Komplexität von Locks oder Synchronisationsprimitive in vielen Fällen vermeiden
  • Der Lebenszyklus der Daten kann klar in eine beschreibbare Phase vor main und eine unveränderliche Phase nach main getrennt werden
  • Wenn man beim Zugriff auf Daten in einem laufenden Programm das Erwerben und Freigeben von Locks vermeiden kann, wird die Struktur einfacher und oft effizienter
  • Das Beispiel verwendet eine Struktur CliSubcommand, eine const-Konstruktorfunktion und #[section], um Subcommands einzusammeln
  • Subcommands wie list, add und help können an beliebigen Stellen im Code stehen
  • Die Funktion main kann dynamisch dispatchen, solange sie nur die Definition der Sektion CLI_SUBCOMMANDS sieht, ohne die Namen oder Positionen registrierter Subcommands zu kennen
  • Wenn keine registrierten Subcommands vorhanden sind, wird auf ein Standard-Subcommand zurückgefallen; im Beispiel fungiert help als Standardwert

Über unveränderliche Daten hinaus

  • Das vorangehende Beispiel setzt voraus, dass gelinkte Daten unveränderlich sind, aber linkerbasierte Datenorganisation kann auch für veränderliche Daten genutzt werden
  • Die Veränderlichkeit globaler statischer Daten ist in Rust ein verbreitetes Problem und kann mit Werkzeugen für innere Veränderlichkeit wie Mutexen oder atomaren Typen gelöst werden
  • Mutexe und atomare Typen sind ohne Konkurrenz nicht teuer, aber auch nicht kostenlos
  • Um Daten in Rust sicher zu verändern, muss die Änderung thread-sicher erfolgen, und es darf keine anderen Referenzen auf dieselben Daten geben, solange eine veränderliche Referenz existiert
  • Die Pre-Main-Umgebung ist Single-Threaded, solange keine Threads ausdrücklich gestartet werden, daher sind keine atomaren Operationen nötig
  • In einer Single-Threaded-Umgebung gilt die happens-before-Beziehung automatisch: Änderungen geschehen vor späteren Lesezugriffen
  • Änderungen an Daten in Link-Sektionen vor main können danach von beliebigen Threads sicher ohne Locks gelesen werden
  • Wenn veränderliche Referenzen nur vor main erzeugt und wieder beendet werden, ist auch die Bedingung erfüllt, dass während ihrer Existenz keine anderen Referenzen vorhanden sind
  • Das Slice einer Link-Sektion ist ein Alias für statische Elemente innerhalb der Sektion, daher gelten Alias-Regeln sowohl für das Slice als auch für die statischen Elemente
  • Um über ein Slice sicher zu verändern, müssen die statischen Elemente zwingend in UnsafeCell liegen
  • Bei statischen Elementen ohne UnsafeCell darf LLVM Werte cachen, umordnen oder Annahmen über die Daten treffen
  • UnsafeCell selbst implementiert nicht Sync, daher ist ein separater Wrapper-Typ nötig
  • Das Beispiel verwendet SyncUnsafeCell und MaybeUninit<SyncUnsafeCell<...>>, um Grenzsymbole und Elemente zu definieren
  • Beim Beispiel eines sortierbaren String-Interning-Pools wird der String-Pool zur Link-Zeit definiert und das Slice früh zur Laufzeit sortiert, damit Strings danach per binärer Suche gefunden werden können
  • Die manuelle Implementierung enthält viel Boilerplate, aber mit ctor und link-section lässt sich dieselbe Struktur mit TypedMutableSection und Konstruktoren deutlich kompakter aufbauen
  • Die Elemente von TypedMutableSection müssen const sein, weil intern Code verwendet wird, der der manuellen Implementierung ähnelt

Vorteile des Link-Sektions-Musters

  • Dieses Muster aggregiert markierte Elemente auf garantierte Weise und platziert alle Daten in vorab allokiertem, zusammenhängendem Speicher
  • Registrierungen können über den gesamten Code verstreut sein
  • Die Anzahl der Elemente in einer Sektion lässt sich garantiert bestimmen
  • Link-Sektionen benötigen keine separate Allokation
  • Ohne Link-Sektionen müsste man für dieselbe Struktur ein HashMap, Vec oder eine andere Datenstruktur allokieren und beim Sammeln der Elemente mehrfach vergrößern
  • Bei traditionellen Sammelansätzen sind die Abhängigkeiten zwischen gemeinsam genutztem Typmodul, beitragenden Modulen und Sammelmodul eng verflochten
  • Mit Link-Sektionen kann der Sammler irgendwo liegen und muss sich nicht darum kümmern, welche Module Daten beisteuern
  • scattered-collect bietet mehrere datenstrukturanaloge Typen mit Unterstützung zur Link-Zeit
    • Scattered*Slice sind verschiedene Vec-ähnliche Strukturen, die ein Slice bereitstellen und optional Sortierung unterstützen
    • ScatteredMap und ScatteredSet sind HashMap-/HashSet-ähnliche Strukturen, die mit minimaler Pre-Main-Initialisierung hashbasierte Key-Value-Lookups bereitstellen

Wann man diesen Ansatz nicht verwenden sollte

  • Berechnungen zur Link-Zeit sind mächtig, aber nicht immer das richtige Werkzeug
  • Anstelle eines Link-Time-Ansatzes kann man Daten auch manuell in einer Crate sammeln, die jede Crate sehen kann, die Daten beitragen möchte
  • Manuelles Sammeln kann unbequem sein und erfordert statt eines einzigen Beitragspunkts in der Kern-Crate oft eine Sammel-Crate mit vielen Crate-Referenzen
  • Dead-Code-Eliminierung wird schwieriger
  • link-section und linkme markieren Elemente mit #[used], daher kann der Linker ungenutzte Daten nicht entfernen
  • Bei kleinen Daten wie interned String-Atomen ist das womöglich kein Problem, aber wenn rohe JSON-/JavaScript-Fragmente oder große Datenstrukturen interned werden, kann sich viel schwer erkennbarer Dead Code ansammeln
  • Pre-Main-Konstruktorfunktionen unterliegen Einschränkungen
  • Konstruktorfunktionen dürfen nicht panicen, und Rust garantiert nicht, dass alle Funktionen der Standardbibliothek verfügbar sind
  • Die Aufrufreihenfolge von Initialisierungsfunktionen mit derselben Priorität ist nicht garantiert und stark plattformabhängig
  • Diese Einschränkungen lassen sich mit sorgfältigem Design umgehen, aber der Pre-Main-Ansatz kann aus subtilen und schwer zu debuggenden Gründen dennoch falsch sein
  • Miri unterstützt nicht alle Pre-Main-Konstruktoren und Konfigurationen von Link-Sektionen vollständig
  • Derzeit betrachtet Miri die Pre-Main-Ausführung nur sehr grundlegend und modelliert Link-Sektionen nicht
  • Für das Testen auf Undefined Behavior werden LLVM-Sanitizer wie ASan und TSan empfohlen
  • Muster der Inversion of Control können es erschweren, alle Stellen zu auditieren, die Daten in Link-Sektionen eintragen
  • Viele weit verbreitete und stark genutzte Rust-Programme sind bereits auf Pre-Main-Funktionen wie ctor, link-section, inventory und linkme angewiesen

Kurze Einordnung zu WASM

  • WASM unterstützt Linker-Sektionen aufgrund früherer Designentscheidungen nicht nativ
  • Der Hinweis #[link_section] platziert Elemente nicht in echten Code-Sektionen, sondern in benutzerdefinierten WASM-Sektionen, auf die der WASM-Code selbst nicht zugreifen kann
  • Die Crate linktime unterstützt WASM und liefert einen emulierten Workaround, damit der Ansatz auch in WASM-Binärdateien funktioniert
  • Eine angemessene WASM-Unterstützung könnte künftig vorgeschlagen werden

Fazit

  • Vor main lässt sich bereits viel Arbeit erledigen, die in bestimmten Fällen erhebliche Vorteile bringt
  • Die Pre-Main-Umgebung ist in ihrer Reihenfolge stark kontrolliert und gut steuerbar, sodass sich viele Aufgaben ohne Locks, atomare Typen oder andere Synchronisationsprimitive sicherer erledigen lassen
  • Linker-Sektionen ermöglichen es, zusammengehörige Daten über die gesamte Binärdatei hinweg beliebig zu aggregieren und gemeinsam zu platzieren, wodurch ungünstige Reihenfolgen bei Crate-Abhängigkeiten vermieden werden
  • In vielen Fällen lassen sich Allokationen vollständig vermeiden, was Abstand zu Problemen von Allokatoren wie Fragmentierung durch wiederholte Allokation schafft
  • Relevante Crates sind ctor, dtor, link-section, scattered-collect

1 Kommentare

 
GN⁺ 4 시간 전
Lobste.rs-Meinungen
  • Go ist insofern auf den meisten Plattformen eine Ausnahme, als es die C-Laufzeit vermeidet, aber Apple verlangt für den Zugriff auf Systemaufrufe eine C-Laufzeit
    Apple nutzt libSystem.dylib als ABI-Stabilitätsgrenze für Systemaufrufe, und Windows der NT-Familie verwendet dafür nicht Systemaufrufe, sondern ntdll.dll: not syscalls
    Auf OpenBSD scheint Go einen Metadaten-Flag gesetzt zu haben, der die Erzwingung des NX-Bits deaktiviert, um die Richtlinie zu umgehen, nach der der Kernel den Prozess beendet, wenn Go Systemaufrufe außerhalb des vom Loader eingerichteten schreibgeschützten libc-Mappings versucht
    Allerdings libSystem.dylib contains the functionality which would normally be libc.so plus other things, daher entspricht es in dieser Hinsicht dem BSD-Ansatz, bei dem „libc die Stabilitätsgrenze“ ist
    Außerdem verwendet Go ab Go 1.16 libc, um OpenBSDs Richtlinie für Systemaufrufe einzuhalten
    Linux ist relativ selten in dem Sinne, dass es stabile Systemaufrufnummern hat, weil es nicht wie andere Betriebssysteme so aufgebaut ist, dass „ein als dynamische Bibliothek in den Adressraum des Prozesses geladener Kernel-Teil sich instabile Systemaufruf-enum-Definitionen mit dem Kernel-Mode-Code teilt“, und weil Linux und glibc nicht wie anderswo gemeinsam im selben Repository entwickelt werden
    Unter Windows übernimmt die C-Laufzeit auch das Parsen einer aus CP/M stammenden Befehlszeichenkette, die über MS-DOS übernommen wurde und von Windows’ API zur Erzeugung von Unterprozessen weitergeführt wird, in ein POSIX-artiges argv-Array
    Deshalb gibt es in der Python-subprocess-Dokumentation den Abschnitt Converting an argument sequence to a string on Windows, der erklärt, wie ein argv-Array nach den im MS-C-Runtime fest verdrahteten Anführungszeichenregeln in eine Zeichenkette umgewandelt wird. Der eigene Parser des aufgerufenen Unterprozesses kann auf Wunsch anders als diese Regeln arbeiten
    Bei Linux bedeutet _start auch nicht exakt, dass der Linker automatisch ein Symbol dieses Namens in die Binärdatei einfügt. Wenn eine Binärdatei im ELF-Format eine ausführbare Datei und keine Bibliothek ist, enthält das Feld e_entry im Header, also an Offset 0x18, die Adresse, zu der der Loader nach dem Einrichten des Speichers springt
    _start ist die GCC-Konvention, um das Ziel anzugeben, auf das e_entry zeigt, wenn man nicht den von libc bereitgestellten Einstiegspunkt verwendet, und Werkzeuge wie NASM folgen dem meines Wissens ebenfalls
    Unter Windows wird _WinMainCRTStartup vom Loader ebenfalls über den AddressOfEntryPoint im PE-Header gefunden. Dieser liegt relativ zum Beginn des PE-Headers bei Offset 0x0028, und der PE-Header kommt nach dem MZ-(DOS-EXE-)Header und dem DOS-Stub
    Um die Details des PE-Headers zu lernen, sind Making the smallest Windows application und Tiny PE gut geeignet. Tiny PE verletzt die PE-Spezifikation teilweise auf eine Weise, die Windows akzeptiert, etwa indem Bereiche überlappt werden, die das Betriebssystem nicht liest, oder Code in ungenutzte Header-Felder gelegt wird. Geht man so weit, hängt die minimale Dateigröße, die Windows akzeptiert, von der jeweils ausgeführten Windows-Version ab
    Zu sehr kleinen ELF-Ausführungsdateien unter Linux ist auch A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux lesenswert
    • Systemaufrufe auf FreeBSD und NetBSD haben ebenso wie die Systembibliotheken ABI-Stabilität
    • In Bezug auf _start: Auf a.out-Systemen war der Einstiegspunkt, an dem der Kernel in die ausführbare Datei sprang, traditionell start, deklariert in csu/crt0. Beispiele sind 7th edition und VAX BSD
      Damals stellte der C-Compiler globalen Symbolen ein _ voran, daher deklariert V7 _main, und bei BSD sieht man, dass der Assemblername für C-start() als undekoriertes start deklariert wurde
      Programme begannen damals am Dateianfang, und der Linker-Aufruf von cc ordnete crt0 so an, dass es ganz vorne stand. csu steht für C startup code, crt0 für das nullte C-Runtime-Unterstützungsobjekt
      Wie genau das unter System V mit dem Aufkommen von ELF funktionierte, ist schwerer herauszufinden, aber start oder _start blieb als Programmeinstiegspunkt in csu/crt0 in Gebrauch
      Ich habe nie ganz verstanden, wie ELF die Behandlung des _-Präfixes verändert hat, aber vermutlich wurde aus einer Laune heraus noch eine weitere Ebene hinzugefügt, sodass start aus irgendeinem Grund zu _start wurde
      Ein klares Gegenstück ist wohl, dass ELF _end hinzufügte; das entspricht dem oberen Ende von BSS und der Position, die sbrk(0) zurückgeben würde, bevor malloc() den Heap anlegt
  • Ich habe mich für das Leben vor main in Rust interessiert und dachte, es wäre gut, in einem Artikel zusammenzufassen, was das ist und warum es nützlich ist
    Es gibt auch Ideen für Folgeartikel, etwa wie man mithilfe der Linker-Aggregation schnellere Collections baut, aber zuerst würde ich gern Feedback zu diesem einsteigerorientierten Thema hören
    • Ich habe viel Embedded Rust gemacht, und daher ist main in Umgebungen mit no_std und manchmal auch ohne alloc nur eine weitere Funktion, und die Initialisierung liegt größtenteils in der Verantwortung des Entwicklers
      Ich habe einige selbstgeschriebene Boilerplate-Muster im Codebestand für ähnliche Zwecke, daher interessiert mich, wie solche Crates mit Embedded-Umgebungen zusammenspielen