2026-03-20
DuckDB.ExtensionKit: DuckDB-Erweiterungen in C# entwickeln
Giorgi Dalakishvili
Einleitung
DuckDB hat einen flexiblen Erweiterungsmechanismus, mit dem Erweiterungen zur Laufzeit dynamisch geladen werden. So lässt sich der Funktionsumfang von DuckDB erweitern, ohne alles ins Hauptbinary zu packen. Erweiterungen können neue Dateiformate, eigene Typen oder neue Scalar- und Tabellenfunktionen beisteuern. Ein großer Teil der DuckDB-Funktionalität steckt selbst in diesem Mechanismus, als Core-Erweiterungen, die das DuckDB-Team parallel zur Engine entwickelt. DuckDB liest und schreibt JSON-Dateien etwa über die json-Erweiterung und spricht mit PostgreSQL über die postgres-Erweiterung.
DuckDB hat außerdem ein lebendiges Ökosystem von Community Extensions, also Drittanbieter-Erweiterungen, die Community-Mitglieder pflegen und die viele Anwendungsfälle und Integrationen abdecken. Zusätzliche Kryptofunktionen stellt zum Beispiel die Community-Erweiterung crypto bereit.
Wie Erweiterungen heute gebaut werden
Heute können Entwickler dieselbe C++-API nutzen, die auch die Core-Erweiterungen verwenden. Eine Vorlage liegt im extension-template-Repository. Die C++-Erweiterungs-API ist zwar mächtig, hängt aber eng an DuckDBs internen APIs und kann (und wird oft) zwischen DuckDB-Versionen wechseln. Außerdem muss man die gesamte DuckDB-Engine bauen, und die Dokumentation ist weniger vollständig als die der C-API.
Dagegen bietet DuckDB ein experimentelles Template für C/C++-Erweiterungen, die gegen die C Extension API von DuckDB linken. Diese API ist eine stabile, rückwärtskompatible Schnittstelle und soll Erweiterungen über DuckDB-Versionen hinweg lauffähig halten. Als C-API lässt sie sich auch aus anderen Sprachen wie Rust nutzen.
Auch mit der C-API bleibt das Schreiben von Erweiterungen Low-Level-Arbeit: manuelles Speichermanagement und viel Boilerplate. Die C-API löst Stabilität und Kompatibilität, nicht aber die Developer Experience in höheren Ökosystemen. Genau hier setzt DuckDB.ExtensionKit an: Es soll die Entwicklung von Erweiterungen für das .NET-Ökosystem zugänglicher machen. Auf der DuckDB C Extension API und mit .NET Native AOT (Ahead-of-Time) kombiniert DuckDB.ExtensionKit beides: native DuckDB-Erweiterungen, die sich wie jede andere Erweiterung einbinden, plus Produktivität und Bibliotheksökosystem von C# und .NET.
DuckDB.ExtensionKit
DuckDB.ExtensionKit stellt C#-APIs und Build-Werkzeuge für DuckDB-Erweiterungen bereit. Es legt die Low-Level-C-Extension-API als C#-Methoden offen und bietet typsichere Higher-Level-APIs für Scalar- und Tabellenfunktionen, liefert aber weiterhin native DuckDB-Erweiterungen. Ein Source Generator erzeugt den nötigen Boilerplate, inklusive nativem Entrypoint und API-Initialisierung.
Mit DuckDB.ExtensionKit ähnelt der Bau einer Erweiterung dem einer gewöhnlichen C#-Bibliothek. Autorinnen legen ein C#-Projekt an, das das ExtensionKit-Runtime referenziert, und implementieren Funktionen über die typsicheren APIs, die DuckDB-Konzepte abbilden.
Zur Build-Zeit emittiert der Source Generator den Boilerplate, inklusive nativem Entrypoint und Erweiterungsinitialisierung. Das Projekt wird dann mit .NET Native AOT kompiliert und erzeugt ein natives DuckDB-Erweiterungsbinary, das DuckDB wie jede andere Erweiterung lädt – ohne .NET-Runtime.
Als konkretes Beispiel zeigt der folgende Ausschnitt eine kleine DuckDB-Erweiterung mit DuckDB.ExtensionKit, die eine Scalar-Funktion und eine Tabellenfunktion für JWTs (JSON Web Token) bereitstellt. Auf hoher Ebene definiert man einen C#-Typ für die Erweiterung und registriert Funktionen explizit. Im Beispiel unten geschieht das über eine partial-Klasse mit dem Attribut [DuckDBExtension] und der Methode RegisterFunctions. Die Implementierung nutzt das NuGet-Paket System.IdentityModel.Tokens.Jwt und zeigt, wie Erweiterungen bestehende .NET-Bibliotheken verwenden können.
Wir fügen zwei Funktionen hinzu: eine Scalar-Funktion zum Extrahieren eines einzelnen Claims aus einem JWT und eine Tabellenfunktion zum Extrahieren mehrerer Claims.
public static partial class JwtExtension{ private static void RegisterFunctions(DuckDBConnection connection) { connection.RegisterScalarFunction<string, string, string?>("extract_claim_from_jwt", ExtractClaimFromJwt);
connection.RegisterTableFunction("extract_claims_from_jwt", (string jwt) => ExtractClaimsFromJwt(jwt), c => new { claim_name = c.Key, claim_value = c.Value }); }
private static string? ExtractClaimFromJwt(string jwt, string claim) { var jwtHandler = new JwtSecurityTokenHandler(); var token = jwtHandler.ReadJwtToken(jwt); return token.Claims.FirstOrDefault(c => c.Type == claim)?.Value; }
private static Dictionary<string, string> ExtractClaimsFromJwt(string jwt) { var jwtHandler = new JwtSecurityTokenHandler(); var token = jwtHandler.ReadJwtToken(jwt); return token.Claims.ToDictionary(c => c.Type, c => c.Value); }}In nur 25 Zeilen haben wir eine Erweiterung gebaut, die DuckDB die Funktionen extract_claim_from_jwt und extract_claims_from_jwt hinzufügt. Wir rufen sie wie jede andere Funktion auf. Um das Feld name aus einem Claim zu holen:
SELECT extract_claim_from_jwt( 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImExZmIyY2NjN2FiMjBiMDYyNzJmNGUxMjIwZDEwZmZlIn0.eyJpc3MiOiJodHRwczovL2lkcC5sb2NhbCIsImF1ZCI6Im15X2NsaWVudF9hcHAiLCJuYW1lIjoiR2lvcmdpIERhbGFraXNodmlsaSIsInN1YiI6IjViZTg2MzU5MDczYzQzNGJhZDJkYTM5MzIyMjJkYWJlIiwiYWRtaW4iOnRydWUsImV4cCI6MTc2NjU5MTI2NywiaWF0IjoxNzY2NTkwOTY3fQ.N7h2xc4rgS4oPo8IO9wyG1lnr2wqTUC80YudWTXp7rXmU2JdsUiweKmuYVVbygdJAR4PJmbQtak4_VuZg2fZFILVpzDyLvGITfUW_18XuDQ_SIm3VlfAuHOVHfruuvvSAfjUkTW2Jlrv3ihFYgusV58vjhcVFHssOGMEbtMNo10Jf62dczVVGNZXh_OOLS0nTLffhY94sZddqQIE56W8xhLK5YMO4gO8voMzhUwDwucnVvyNfui38MPDNdTSKjn3Ab0hG8jzOVhbYSCHf0eQsbxPzGtXUCJobScWDb78IphFWec6W4ugIYp5CMh3C_noQi94NYjQg2P-AJ5FLCKzKA', 'name');Das liefert Giorgi Dalakishvili. Testen wir die Tabellenfunktion:
SELECT *FROM extract_claims_from_jwt( 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImExZmIyY2NjN2FiMjBiMDYyNzJmNGUxMjIwZDEwZmZlIn0.eyJpc3MiOiJodHRwczovL2lkcC5sb2NhbCIsImF1ZCI6Im15X2NsaWVudF9hcHAiLCJuYW1lIjoiR2lvcmdpIERhbGFraXNodmlsaSIsInN1YiI6IjViZTg2MzU5MDczYzQzNGJhZDJkYTM5MzIyMjJkYWJlIiwiYWRtaW4iOnRydWUsImV4cCI6MTc2NjU5MTI2NywiaWF0IjoxNzY2NTkwOTY3fQ.N7h2xc4rgS4oPo8IO9wyG1lnr2wqTUC80YudWTXp7rXmU2JdsUiweKmuYVVbygdJAR4PJmbQtak4_VuZg2fZFILVpzDyLvGITfUW_18XuDQ_SIm3VlfAuHOVHfruuvvSAfjUkTW2Jlrv3ihFYgusV58vjhcVFHssOGMEbtMNo10Jf62dczVVGNZXh_OOLS0nTLffhY94sZddqQIE56W8xhLK5YMO4gO8voMzhUwDwucnVvyNfui38MPDNdTSKjn3Ab0hG8jzOVhbYSCHf0eQsbxPzGtXUCJobScWDb78IphFWec6W4ugIYp5CMh3C_noQi94NYjQg2P-AJ5FLCKzKA');Das liefert:
| claim_name | claim_value |
|---|---|
| iss | https://idp.local |
| aud | my_client_app |
| name | Giorgi Dalakishvili |
| sub | 5be86359073c434bad2da3932222dabe |
| admin | true |
| exp | 1766591267 |
| iat | 1766590967 |
Wie DuckDB.ExtensionKit funktioniert
DuckDB.ExtensionKit stützt sich auf mehrere moderne C#-Sprach- und Runtime-Features, um DuckDBs C-Erweiterungs-API effizient an Managed Code zu koppeln. So lassen sich native Erweiterungen in C# bauen, ohne zur Ladezeit eine Managed Runtime vorauszusetzen.
Funktionszeiger
DuckDBs C-Erweiterungs-API liegt als versionierte Funktionstabelle vor: ein großes Struct (duckdb_ext_api_v1), dessen Felder C-Funktionszeiger sind (z. B. duckdb_open, duckdb_register_scalar_function, duckdb_vector_get_data usw.). DuckDB.ExtensionKit spiegelt das in C#. Es definiert eine C#-Darstellung des Structs (DuckDBExtApiV1), in der jedes Feld ein C#-Funktionszeiger ist (delegate* unmanaged[Cdecl]<...>). Das mappt die C-ABI direkt: der Aufruf in DuckDB wird ein einfacher indirekter Call über ein Funktionszeigerfeld, kein Delegate-Aufruf mit Runtime-Marshaling.
Entrypoint
Eine DuckDB-Erweiterung muss eine Entrypoint-Funktion nach der C-Aufrufkonvention exportieren (der exportierte Name ist der Name der Erweiterung plus _init_c_api). So findet DuckDB sie beim Laden. Im C-Erweiterungstemplate erledigen das Makros, die die exportierte Funktion und den umgebenden Boilerplate erzeugen.
DuckDB.ExtensionKit folgt demselben Modell, erzeugt den Boilerplate aber aus C# statt aus C-Makros. Der Source Generator emittiert einen nativ kompatiblen Entrypoint, der die API-Tabelle (über das access-Objekt) holt und die nötige Initialisierung ausführt, genau wie das C-Template. Die erzeugte Methode trägt [UnmanagedCallersOnly(EntryPoint = "...")], wodurch die .NET-Toolchain ein echtes natives Symbol mit diesem Namen exportiert, das von C aus aufrufbar ist. Mit .NET Native AOT wird das eine tatsächlich exportierte Funktion im Binary – DuckDB lädt und ruft die Erweiterung genau so, als wäre sie in C geschrieben.
Native AOT
Schließlich macht Native AOT den Ansatz für DuckDB-Erweiterungen praktikabel. Sobald Erweiterungscode und generierte Quellen kompiliert sind, wird das Projekt mit .NET Native AOT veröffentlicht. Dabei entsteht ein natives Binary ohne Abhängigkeit von einer Managed Runtime zur Ladezeit. Das Artefakt ist eine native DuckDB-Erweiterung, die sich wie in C oder C++ geschriebene Erweiterungen laden und ausführen lässt. Aus DuckDBs Sicht gibt es keinen Unterschied zwischen einer Erweiterung aus DuckDB.ExtensionKit und einer in einer klassischen nativen Sprache.
Aktueller Stand und Einschränkungen
DuckDB.ExtensionKit ist, wie das C-Erweiterungstemplate, derzeit experimentell. Die APIs entwickeln sich noch, und nicht alle von DuckDB unterstützten Erweiterungsfunktionen sind bereits freigegeben.
Das Toolkit setzt auf .NET Native AOT, Erweiterungen müssen also für konkrete Zielplattformen gebaut werden (zum Beispiel linux-x64, osx-arm64 oder win-x64). Wie bei anderen nativen Erweiterungen sind Binaries plattformspezifisch und müssen entsprechend gebaut werden.
Bauen Sie Ihre eigene Erweiterung in C#
DuckDB.ExtensionKit ist ein Open-Source-Projekt auf GitHub unter der MIT-Lizenz. Das Projekt enthält Beispielerweiterungen, die zeigen, wie man DuckDB-Erweiterungen in C# definiert und baut. Im Repository liegt eine JWT-Beispielerweiterung mit Scalar- und Tabellenfunktionen sowie dem vollständigen Build- und Publishing-Workflow mit .NET Native AOT.
Feedback, Bugreports und Beiträge sind über GitHub Issues willkommen.
Schlussgedanken
DuckDBs Erweiterungsmechanismus hat sich als flexible Grundlage erwiesen, das System zu erweitern, ohne die Core-Engine zu belasten. DuckDB.ExtensionKit erkundet, wie dieser Mechanismus über das .NET-Ökosystem einem größeren Kreis zugänglich wird und trotzdem native Erweiterungen liefert, die sich direkt in DuckDB einhängen.
Obwohl C# meist als High-Level-Sprache gilt, zeigt dieses Projekt, dass sich damit bei Bedarf auch Low-Level-, ABI-kompatible Komponenten bauen lassen. Mit modernen C#-Features und DuckDBs bestehender Erweiterungsschnittstelle kann man Erweiterungen in einer High-Level-Sprache schreiben, ohne die Kontrolle über die nativen Grenzen aufzugeben.