Moderne React-Apps mit Legacy COM/ActiveX-Systemen über Rust FFI verbinden
Erfahren Sie, wie Sie moderne React-Apps mit Legacy COM/ActiveX-Systemen über Rust FFI, C++-Adapter und Tauri verbinden. Praxisnahe Enterprise-Integrationslösung.
Der Kontext: Wenn alte und neue Welten aufeinandertreffen
Wir alle kennen das - man entwickelt eine schicke, moderne Webanwendung mit dem neuesten Tech-Stack (TS, React, Next.js), und dann kommt der Kunde mit der Überraschung: "Ach übrigens, das muss mit unserem ERP-System von 2008 integriert werden."
In meinem Fall nutzte der Kunde ein veraltetes ERP-System, das seine Funktionalität nur über eine COM/ActiveX-API via "EDP-Schnittstelle" bereitstellte - ein proprietärer (32-Bit) Treiber, der lokal auf jedem Windows-PC der Benutzer installiert werden musste. Währenddessen entwickelte ich eine moderne TypeScript-Anwendung mit React und Next.js.
Die Herausforderung? Diese beiden Welten sprechen von Natur aus nicht miteinander.
Mein erster Instinkt wäre vielleicht Electron gewesen, aber ich entschied mich für Tauri. Tauri bietet nicht nur bessere Performance mit kleineren Bundle-Größen und geringerem Speicherbedarf, sondern ich hatte auch ein persönliches Interesse daran, meine Rust-Kenntnisse zu vertiefen. Diese Entscheidung sollte sich als instrumental für die Lösung des Integrationspuzzles erweisen.
Der Angriffsplan: Eine Brücke zwischen Technologien bauen
Die Lösung erforderte ein mehrschichtiges Adapter-Pattern, das im Grunde eine Kette von Übersetzern zwischen der modernen Web-App und der Legacy-COM-Schnittstelle erstellt:
TypeScript / React
↑
└────┐
↓
Tauri (Rust)
↑
└────┐
↓
C++ DLL
↑
└────┐
↓
COM / ActiveX ERP
So funktioniert der Datenfluss:
Connection-Instanz erstellen
Zuerst stellen wir eine Verbindung zum ERP-System her. Diese Verbindung muss sorgfältig verwaltet werden - mehr dazu später.Query aufbauen
Das ERP-System erwartet Abfragen in seinem eigenen Format, ähnlich wie SQL, aber mit seinen Eigenarten. Wir konstruieren diese Suchstrings in Rust, bevor wir sie weitergeben.Die Unsafe-Brücke
Hier wird es interessant. Wir machen einen unsafe FFI-Aufruf an unsere C++-Adapterfunktion über die C-Schnittstelle. Die entscheidende Erkenntnis: Wir übergeben zwei Callback-Funktionen:
//Rust
let mut logs: Vec<LogEntry> = vec![];
let mut records: Vec<Record> = vec![];
unsafe {
// Vereinfachtes Beispiel
fetch_from_erp(
connection.as_ptr(),
query_cstring.as_ptr(),
&mut logs as *mut Vec<LogEntry> as *mut c_void,
log_callback,
&mut records as *mut Vec<Record> as *mut c_void,
add_record,
);
}
Die Callbacks und ihre Datenvektoren sind entscheidend - sie ermöglichen es der C++-Schicht, Daten asynchron zurück an Rust zu senden, während die COM-Schnittstelle Ergebnisse liefert.
Ergebnisse sammeln
Nach der Ausführung des unsafe-Blocks haben wir zweiVec-Collections:logs: Enthält alle Logging-Informationen, einschließlich Errorsrecords: Enthält die tatsächlichen Abfrageergebnisse
Rückgabe an TypeScript/Next.js
Schließlich serialisieren wir die Daten als JSON (Tauri v1.x) und geben sie über Tauris IPC-Mechanismus an das TypeScript-Frontend zurück, oder geben einenErrmit einer aussagekräftigen Fehlermeldung zurück, falls etwas schiefgelaufen ist.
Die Schwierigkeiten: Wo es knifflig wurde
Connection-Verwaltung: COM-Lifecycle managen
Die ERP-Verbindung erfordert eine sorgfältige Lifecycle-Verwaltung.
Auf der C++-Seite habe ich die COM-EDP-Schnittstelle in einer RAII-Klasse gekapselt,
die ConnectionInitialize/ConnectionUninitialize verwaltet:
//C++
class Connection {
EDP* edp;
public:
Connection() { ConnectionInitialize(); }
~Connection() { ConnectionUninitialize(); }
EDPQueryPtr createQuery() { return edp->CreateQuery(); }
};
// C-Schnittstelle für FFI
extern "C" {
Connection* create_connection(...);
void close_connection(Connection* conn);
}
Auf der Rust-Seite wird dies zu einem einfachen Wrapper mit automatischer Bereinigung via Drop.
Die entscheidende Erkenntnis: Die Connection erstellt Query-Objekte, die Referenzen zu ihrem Parent behalten,
was ein korrektes COM-Lifecycle-Management durch Rusts Ownership-System sicherstellt.
Datenfluss durch die Schichten: Die echte Herausforderung
Daten von COM über C++ nach Rust und schließlich zu TypeScript zu bekommen, war das interessanteste Puzzle. Die Lösung dreht sich um Callbacks, die Daten in Rust-eigenen Vektoren sammeln.
Das Callback-Pattern
Die Schlüsselerkenntnis ist die Verwendung von Rust-Funktionen als C-Callbacks zum Befüllen von Vektoren:
//Rust
extern "C" fn log_message(str_ptr: *const c_char, level: c_int, ctx: *mut c_void) {
unsafe {
let logs = &mut *(ctx as *mut Vec<LogEntry>);
let message = CStr::from_ptr(str_ptr).to_string_lossy();
logs.push(LogEntry { level, message: message.into() });
}
}
extern "C" fn add_record(id: c_int, name: *const c_char, value: c_float, ctx: *mut c_void) {
unsafe {
let records = &mut *(ctx as *mut Vec<Record>);
records.push(Record {
id,
name: CStr::from_ptr(name).to_string_lossy().into(),
value,
});
}
}
Der ctx-Pointer ist entscheidend - er wird zurück zu einer veränderlichen Referenz auf unseren Rust-Vektor
gecastet, was es der C++-Schicht ermöglicht, Daten zu pushen, ohne Rusts Speichermodell zu kennen.
Die Orchestrierung
Vom Tauri-Command-Handler aus richten wir die Vektoren ein und machen den unsafe-Aufruf:
//Rust
#[tauri::command]
pub async fn fetch_data(password: &str) -> Result<String, String> {
let mut logs: Vec<LogEntry> = vec![];
let mut records: Vec<Record> = vec![];
let mut connection = Connection::new().map_err(|e| e.to_string())?;
connection.open(password, &mut logs);
let search = CString::new("search_criteria").unwrap();
unsafe {
fetch_from_erp(
connection.as_ptr(),
search.as_ptr(),
&mut logs as *mut Vec<LogEntry> as *mut c_void,
log_message,
&mut records as *mut Vec<Record> as *mut c_void,
add_record,
);
}
// Logs vor der Rückgabe auf Fehler prüfen
if let Some(error) = logs.iter().find(|l| l.level > 0) {
return Err(error.message.clone());
}
serde_json::to_string(&records).map_err(|e| e.to_string())
}
Die C++ Seite
Die C++-Schicht iteriert durch COM-Query-Ergebnisse und ruft unsere Callbacks für jeden Datensatz auf:
//C++
void fetch_from_erp(Connection* conn, const char* search,
void* log_ctx, LogCallback log_cb,
void* data_ctx, DataCallback data_cb) {
QueryPtr query = conn->createQuery();
if (!query->StartQuery("TABLE", "id,name,value", search)) {
_bstr_t error = query->GetLastError();
log_cb(error, 1, log_ctx); // Level 1 === Fehler
return;
}
while (query->GetNextRecord()) {
int id = bstr2int(query->GetFieldN("id"));
_bstr_t name = query->GetFieldN("name");
float value = bstr2float(query->GetFieldN("value"));
data_cb(id, name, value, data_ctx);
}
}
Die Schönheit dieses Patterns liegt in seiner Einfachheit: Rust besitzt den Speicher, C++ füllt ihn nur über Callbacks. Kein komplexes Speichermanagement, keine manuelle Bereinigung - nur Daten, die durch Funktionszeiger in vorallokierte Vektoren fließen. Die finale JSON-Serialisierung geschieht komplett in Rust und gibt uns Typsicherheit bis zur TypeScript-Grenze.
Fehlerbehandlung: Fehler hilfreich machen
COM-Fehler sind notorisch kryptisch - ein fehlschlagender Aufruf könnte 0x80004005 (E_FAIL) ohne Kontext zurückgeben.
Ich habe dies durch einen mehrschichtigen Ansatz gelöst:
Die C++-Schicht fängt COM-Fehler ab und loggt sie sofort über den Callback mit menschenlesbarem Kontext:
//C++
if (!query->StartQuery(...)) {
// Tatsächliche ERP-Fehlermeldung holen
_bstr_t error = query->GetLastError();
log_cb(L"Query fehlgeschlagen: " + error, 1, log_ctx);
}
Die Rust-Schicht prüft den Logs-Vektor nach jedem unsafe-Aufruf und konvertiert Fehler in ordentliche Result-Typen:
//Rust
if let Some(error) = logs.iter().find(|l| l.level > 0) {
return Err(error.message.clone());
}
Dieses Pattern stellt sicher, dass Fehler mit aussagekräftigen Nachrichten hochbubbeln anstatt kryptischer COM-Codes. Bis ein Fehler das TypeScript-Frontend erreicht, enthält er verwertbare Informationen wie "Query fehlgeschlagen: Ungültiger Feldname 'foo'" anstatt "0x80004005".
Lessons Learned
Der Bau dieser Brücke hat mich mehrere wertvolle Lektionen gelehrt:
Legacy-Systeme sprechen eine andere Sprache - buchstäblich und konzeptionell. COM erwartet synchrone, zustandsbehaftete Interaktionen mit manuellem Speichermanagement. Moderne Web-Apps erwarten asynchrone, zustandslose Operationen mit automatischer Bereinigung. Die Adapter-Schicht übersetzt nicht nur Datenformate; sie übersetzt ganze Programmierparadigmen.
Callbacks sind dein Freund für FFI. Anstatt zu versuchen, komplexe Datenstrukturen über Sprachgrenzen hinweg zu implementieren, lässt das Callback-Pattern die Speicherverwaltung einfach halten. Rust besitzt die Vektoren, C++ füllt sie nur. Kein geteilter Speicher, keine Bereinigungs-Koordination - nur Funktionszeiger und Kontextzeiger.
Tauri + Rust war die perfekte Wahl. Rusts unsafe-Blöcke machen die gefährlichen Teile explizit und eingedämmt,
während das Ownership-System überall sonst ordnungsgemäße Bereinigung sicherstellt.
Wenn die Connection out of scope geht, löst sie automatisch die gesamte COM-Bereinigungskette aus.
Versuch das mal zuverlässig in JavaScript!
Gute Fehlermeldungen sind den Aufwand wert. Jede Schicht, die Fehler übersetzt - von COM HRESULT zu C++-String zu Rust Result zu benutzerfreundlicher Nachricht - ist eine Gelegenheit, Kontext hinzuzufügen. Dein zukünftiges Debugging-Ich wird es dir danken.
Manchmal sind die interessantesten Engineering-Herausforderungen nicht der Einsatz modernster Technologie -
sie handeln davon, dass sich ein 15 Jahre altes ERP-System nativ in einer modernen React-App anfühlt.
Es ist durchaus befriedigend, unsafe-Blöcke zu schreiben, die Legacy-Systeme "safe" nutzbar machen.