Appeler une DLL C# Native AOT depuis C/C++
· Mis à jour le: · Go Komura · C#, .NET, Native AOT, C++, Développement Windows, Interopérabilité native
Dans l’article précédent, Pourquoi un wrapper C++/CLI est un choix efficace pour utiliser des DLL natives depuis C#, nous avions fait le point sur la frontière lorsqu’on appelle du C++ depuis C#. Cette fois, nous inversons le sens : il s’agit d’appeler du C# depuis C/C++.
Il arrive qu’on veuille appeler depuis une application C/C++ existante un traitement écrit en C#, sans que P/Invoke ne convienne — il va dans l’autre sens — ni que cela justifie de faire intervenir C++/CLI ou COM. C’est particulièrement le cas quand on veut conserver l’application native telle quelle, en ne déplaçant vers C# que des éléments comme la logique de décision, le traitement de chaînes, l’interprétation de la configuration ou les règles de calcul.
COM peut aussi servir de pont, mais nous adoptons ici une approche plus in-process, plus proche d’une véritable DLL. Avec Native AOT de .NET, vous pouvez publier une bibliothèque de classes comme bibliothèque partagée native, et exposer les méthodes marquées UnmanagedCallersOnly comme points d’entrée C. Autrement dit, vous pouvez utiliser C# comme « la DLL native appelée ».
Cela dit, tout ne peut pas franchir la frontière tel quel. Laisser fuir string, List<T>, des exceptions ou la propriété d’objets à la frontière fait rapidement dégénérer l’ambiance. Dans cet article, à l’aide d’un exemple minimal Windows + C++, nous examinons dans quels cas cette configuration est particulièrement efficace et quelles formes d’API restent robustes. Le raisonnement est quasiment le même sous Linux / macOS, mais les exemples de code partent du principe d’une DLL Windows.
Par ailleurs, tout le code présenté dans cet article est publié sur GitHub sous la forme d’un ensemble d’exemples buildable et exécutable (la bibliothèque C# publiée avec Native AOT, un exemple d’appel en C++ et des tests unitaires).
csharp-native-aot-native-dll-from-c-cpp - komurasoft-blog-samples (GitHub)
Table des matières
- D’abord la conclusion (en une ligne)
- Choisir le bon pont
- Schéma d’architecture
- Configuration minimale
- 4.1. Le projet C#
- 4.2. Le code C# exporté
- 4.3. La commande de publication
- 4.4. L’appel côté C++
- Des formes d’API résistantes
- 5.1. Se rapprocher de l’ABI C
- 5.2. Traiter les chaînes comme pointeur + longueur + capacité de buffer
- 5.3. Ne jamais laisser les exceptions franchir la frontière
- 5.4. Fixer la convention d’appel
- 5.5. Garder les méthodes exportées minces et séparer le corps de la logique
- Les cas où cela convient
- Les cas où cela ne convient toujours pas
- Pièges
- Résumé
- Références
1. D’abord la conclusion (en une ligne)
- Si vous voulez appeler un traitement C# depuis C/C++ en in-process, Native AOT +
UnmanagedCallersOnlyest une option très solide. - Cependant, ce qui est exporté n’est jamais qu’un point d’entrée de fonction C. Ce n’est pas un monde où l’on expose directement
stringouList<T>. - En pratique, il est plus stable de ramener les choses à une API C plate du type
create/destroy/operate, en explicitant la gestion de la durée de vie et les codes d’erreur. - Si vous voulez manipuler naturellement des classes C++ et la STL, C++/CLI est plus adapté ; si vous avez besoin d’enregistrement, d’automatisation ou d’appels inter-processus, COM est le meilleur choix.
En résumé : vous pouvez utiliser C# comme contenu d’une DLL native, mais la frontière doit être conçue comme une ABI C, et non comme du .NET. Si vous acceptez ce compromis, cela devient une arme vraiment intéressante.
2. Choisir le bon pont
| Ce que vous voulez faire | Candidat solide | Pourquoi |
|---|---|---|
| Appeler un ensemble de fonctions C depuis C# | P/Invoke | Le sens est direct, c’est l’approche la plus naturelle |
| Manipuler naturellement une bibliothèque C++ depuis C# | C++/CLI | Les types C++, la propriété, les exceptions, std::wstring, etc. sont faciles à absorber côté C++ |
| Franchir la frontière 32 bits / 64 bits ou une frontière de processus | COM / IPC | Une DLL in-process seule ne peut pas franchir ces frontières |
| Appeler une logique C# depuis C/C++ comme une DLL native | Native AOT + UnmanagedCallersOnly |
Vous pouvez exporter vous-même vos propres points d’entrée C |
Cette configuration est particulièrement efficace dans les scénarios où « le côté natif est le protagoniste, et C# n’est appelé que comme un composant ». C’est exactement l’inverse de P/Invoke ou de C++/CLI.
3. Schéma d’architecture
flowchart LR
Cpp["Application C / C++"] -->|appels de fonction cdecl| Dll["DLL C# publiée avec Native AOT"]
Dll --> Exports["Exports marqués UnmanagedCallersOnly"]
Exports --> Core["Logique métier C#"]
Exports --> Store["Table de handles / gestion d'état"]
L’apparence est simple. Ce qui compte, c’est d’aligner la frontière sur des fonctions C. Peu importe que l’implémentation interne côté C# utilise des classes, des collections ou LINQ : la surface exposée à l’extérieur reste plate.
4. Configuration minimale
Ici, nous prenons un exemple minimal où le côté C++ crée un « accumulateur », y ajoute des valeurs, puis récupère le total à la fin. En pratique, il pourrait tout aussi bien s’agir d’un moteur de décision, de l’interprétation d’une configuration ou d’un analyseur simple. Considérez-le comme le schéma où le côté natif détient un handle et appelle les fonctions d’opération dans l’ordre.
4.1. Le projet C#
Commençons par préparer une bibliothèque de classes.
<!-- NativeAotSample.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
Il y a deux points clés.
- Activer la publication Native AOT
- Autoriser
unsafe, puisque nous utilisons des arguments de type pointeur
Les exemples de cet article partent du principe de net8.0, mais le raisonnement reste le même pour .NET 9 / 10.
4.2. Le code C# exporté
Les méthodes marquées UnmanagedCallersOnly deviennent les points d’entrée visibles depuis le côté natif. Ici, nous distribuons des handles sous forme d’entiers, et l’état interne est géré dans un dictionnaire côté C#.
// NativeExports.cs
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace KomuraSoft.NativeAotSample;
internal static class NativeStatus
{
public const int Ok = 0;
public const int InvalidArgument = -1;
public const int InvalidHandle = -2;
public const int UnexpectedError = -3;
}
internal sealed class Accumulator
{
public long Total { get; private set; }
public void Add(int value)
{
Total += value;
}
}
internal static class AccumulatorStore
{
private static readonly object s_gate = new();
private static readonly Dictionary<nint, Accumulator> s_instances = new();
private static long s_nextHandle = 0;
public static int Create(out nint handle)
{
try
{
var instance = new Accumulator();
handle = (nint)System.Threading.Interlocked.Increment(ref s_nextHandle);
lock (s_gate)
{
s_instances.Add(handle, instance);
}
return NativeStatus.Ok;
}
catch
{
handle = 0;
return NativeStatus.UnexpectedError;
}
}
public static int Add(nint handle, int value)
{
try
{
lock (s_gate)
{
if (!s_instances.TryGetValue(handle, out var instance))
{
return NativeStatus.InvalidHandle;
}
instance.Add(value);
return NativeStatus.Ok;
}
}
catch
{
return NativeStatus.UnexpectedError;
}
}
public static int GetTotal(nint handle, out long total)
{
try
{
lock (s_gate)
{
if (!s_instances.TryGetValue(handle, out var instance))
{
total = 0;
return NativeStatus.InvalidHandle;
}
total = instance.Total;
return NativeStatus.Ok;
}
}
catch
{
total = 0;
return NativeStatus.UnexpectedError;
}
}
public static int Destroy(nint handle)
{
try
{
lock (s_gate)
{
return s_instances.Remove(handle)
? NativeStatus.Ok
: NativeStatus.InvalidHandle;
}
}
catch
{
return NativeStatus.UnexpectedError;
}
}
}
public static unsafe class NativeExports
{
[UnmanagedCallersOnly(
EntryPoint = "km_accumulator_create",
CallConvs = new[] { typeof(CallConvCdecl) })]
public static int AccumulatorCreate(nint* outHandle)
{
if (outHandle == null)
{
return NativeStatus.InvalidArgument;
}
var status = AccumulatorStore.Create(out var handle);
*outHandle = handle;
return status;
}
[UnmanagedCallersOnly(
EntryPoint = "km_accumulator_add",
CallConvs = new[] { typeof(CallConvCdecl) })]
public static int AccumulatorAdd(nint handle, int value)
{
return AccumulatorStore.Add(handle, value);
}
[UnmanagedCallersOnly(
EntryPoint = "km_accumulator_get_total",
CallConvs = new[] { typeof(CallConvCdecl) })]
public static int AccumulatorGetTotal(nint handle, long* outTotal)
{
if (outTotal == null)
{
return NativeStatus.InvalidArgument;
}
var status = AccumulatorStore.GetTotal(handle, out var total);
*outTotal = total;
return status;
}
[UnmanagedCallersOnly(
EntryPoint = "km_accumulator_destroy",
CallConvs = new[] { typeof(CallConvCdecl) })]
public static int AccumulatorDestroy(nint handle)
{
return AccumulatorStore.Destroy(handle);
}
}
Ce qui est fait est plutôt simple.
- Seul un handle
intptr_test montré au côté natif - L’état réel est détenu côté C#
- create / add / get / destroy sont décomposés en fonctions plates
- Les valeurs de retour sont des codes d’erreur, et les valeurs de sortie reviennent via des arguments pointeurs
Avec cette forme, même si vous remplacez ensuite l’implémentation interne côté C#, l’ABI côté C reste remarquablement stable.
4.3. La commande de publication
Publions-la comme bibliothèque partagée.
dotnet publish -r win-x64 -c Release /p:NativeLib=Shared
Cela produit une DLL native sous bin/Release/net8.0/win-x64/publish/. Pour Windows, c’est un .dll ; pour Linux, un .so ; pour macOS, un .dylib.
Ce qui compte, c’est de publier par RID. Un binaire produit pour win-x64 ne peut pas être utilisé comme s’il était pour win-arm64, et la bitness de l’appelant et de la DLL doivent également correspondre.
4.4. L’appel côté C++
Pour l’instant, laissons de côté la question des bibliothèques d’import, et appelons directement via LoadLibrary / GetProcAddress. Sous cette forme, il est facile de voir ce qui est exporté et avec quelle signature il faut le recevoir.
/* native_api.h */
#pragma once
#include <stdint.h>
enum km_status
{
KM_STATUS_OK = 0,
KM_STATUS_INVALID_ARGUMENT = -1,
KM_STATUS_INVALID_HANDLE = -2,
KM_STATUS_UNEXPECTED_ERROR = -3
};
typedef int (__cdecl *km_accumulator_create_fn)(intptr_t* out_handle);
typedef int (__cdecl *km_accumulator_add_fn)(intptr_t handle, int value);
typedef int (__cdecl *km_accumulator_get_total_fn)(intptr_t handle, int64_t* out_total);
typedef int (__cdecl *km_accumulator_destroy_fn)(intptr_t handle);
// main.cpp
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <windows.h>
#include "native_api.h"
template <typename T>
T LoadSymbol(HMODULE module, const char* name)
{
FARPROC proc = ::GetProcAddress(module, name);
if (proc == nullptr)
{
std::cerr << "GetProcAddress failed: " << name << '\n';
std::exit(EXIT_FAILURE);
}
return reinterpret_cast<T>(proc);
}
int main()
{
HMODULE module = ::LoadLibraryW(L"NativeAotSample.dll");
if (module == nullptr)
{
std::cerr << "LoadLibraryW failed" << '\n';
return EXIT_FAILURE;
}
auto create = LoadSymbol<km_accumulator_create_fn>(module, "km_accumulator_create");
auto add = LoadSymbol<km_accumulator_add_fn>(module, "km_accumulator_add");
auto getTotal = LoadSymbol<km_accumulator_get_total_fn>(module, "km_accumulator_get_total");
auto destroy = LoadSymbol<km_accumulator_destroy_fn>(module, "km_accumulator_destroy");
intptr_t handle = 0;
if (create(&handle) != KM_STATUS_OK)
{
std::cerr << "create failed" << '\n';
return EXIT_FAILURE;
}
if (add(handle, 10) != KM_STATUS_OK)
{
std::cerr << "add(10) failed" << '\n';
return EXIT_FAILURE;
}
if (add(handle, 20) != KM_STATUS_OK)
{
std::cerr << "add(20) failed" << '\n';
return EXIT_FAILURE;
}
std::int64_t total = 0;
if (getTotal(handle, &total) != KM_STATUS_OK)
{
std::cerr << "get_total failed" << '\n';
return EXIT_FAILURE;
}
std::cout << "total = " << total << '\n';
if (destroy(handle) != KM_STATUS_OK)
{
std::cerr << "destroy failed" << '\n';
return EXIT_FAILURE;
}
handle = 0;
// Ne pas utiliser de bibliothèque partagée Native AOT en prévoyant de la décharger.
// FreeLibrary(module);
return EXIT_SUCCESS;
}
Dans cet exemple, tout ce que voit le côté C++ est « une API C appelable via des pointeurs de fonction ». Le fait que l’intérieur soit écrit en C# n’a presque pas besoin d’être pris en compte.
5. Des formes d’API résistantes
Pouvoir exporter avec Native AOT est amusant, mais en pratique, ce que l’on choisit de ne pas exporter compte davantage.
5.1. Se rapprocher de l’ABI C
Il est plus serein de restreindre dès le départ les types exposés à la frontière à peu près à ceci.
- Des types primitifs comme
int32_t/int64_t/double - Des structs à layout fixe
- Des handles équivalents à
intptr_t/void* uint8_t*accompagné d’une longueur
À l’inverse, voici ce qu’il ne faut surtout pas laisser fuir dès le départ.
stringobjectList<T>TaskSpan<T>- Les classes C++,
std::vector,std::wstring
Essayer de faire franchir la frontière à ces éléments tels quels trouble rapidement la surface de la frontière. L’important est de ne pas laisser fuir les contraintes de C# vers C++, et de ne pas non plus laisser fuir excessivement les contraintes de C++ vers C#.
5.2. Traiter les chaînes comme pointeur + longueur + capacité de buffer
Dès que l’on veut échanger des chaînes, on a envie d’exposer directement string, mais il vaut mieux résister à cette tentation. À la frontière d’une bibliothèque, il est plus clair de ramener cela à une forme comme la suivante, par exemple.
int km_parse_utf8(const uint8_t* text, int32_t text_len, int32_t* out_value);
int km_format_utf8(int32_t value, uint8_t* buffer, int32_t buffer_len, int32_t* out_written);
Autrement dit, décidez à l’avance de l’encodage des caractères, de la longueur, et de qui alloue le buffer. Comme il s’agit de Windows, se rapprocher d’UTF-16 est une option, mais si vous envisagez d’autres langages, UTF-8 est souvent plus facile à manipuler.
5.3. Ne jamais laisser les exceptions franchir la frontière
Une frontière de fonction native n’est pas un moyen très accueillant pour représenter des exceptions. Il est au moins plus sûr de ne pas concevoir les choses de façon à laisser une exception managée fuir directement vers l’appelant.
En pratique, il est plus facile à gérer de procéder ainsi :
- La valeur de retour est un code de statut
- Les données réelles reviennent via des buffers de sortie ou des arguments pointeurs
- Si nécessaire, récupérer des informations supplémentaires via une fonction de type
get_last_error
Ce n’est pas spectaculaire, mais ce genre de conception discrète paie plus tard. Autrement dit, ne commencez pas soudainement un combat au corps à corps à la frontière.
5.4. Fixer la convention d’appel
Dans l’exemple, CallConvCdecl a été spécifié explicitement. Si vous l’omettez, vous obtenez la convention d’appel par défaut de la plateforme, mais si vous voulez fixer l’en-tête et les types de pointeurs de fonction, il est moins accidentogène de le déclarer vous-même explicitement.
En particulier s’il existe une possibilité de traiter avec x86, laisser ce point ambigu deviendra pénible plus tard. Même si cela se manifeste rarement en x64, mieux vaut fixer la règle dès le départ.
5.5. Garder les méthodes exportées minces et séparer le corps de la logique
Les méthodes marquées UnmanagedCallersOnly ne sont pas censées être appelées directement depuis du code managé ordinaire. Donc si vous commencez à y écrire toute votre logique métier, les tests deviennent aussi difficiles.
Dans l’exemple aussi, la gestion réelle de l’état se trouve dans AccumulatorStore, et le NativeExports exporté n’est qu’une entrée mince. C’est un point assez important.
- Méthodes exportées : le guichet de l’ABI
- Classes internes : la logique C# ordinaire
Avec cette répartition des rôles, vous pouvez réfléchir séparément à la frontière avec C++ et au code principal en C#.
6. Les cas où cela convient
Cette configuration s’intègre particulièrement bien dans des scénarios comme ceux-ci.
- Vous voulez conserver l’application C/C++ existante telle quelle, en ne déplaçant vers C# qu’une partie de la logique métier
- Vous ne voulez pas que la préinstallation du runtime .NET soit un prérequis de distribution
- Vous pouvez garder petite la surface des fonctions exportées
- Vous pourriez, à l’avenir, vouloir appeler la même API C depuis d’autres langages comme Rust ou Go
Cela s’accorde particulièrement bien avec une configuration où l’on garde l’application native telle quelle, et où l’on écrit en C# uniquement la couche de logique facile à remplacer. L’UI et le pilotage des équipements restent en C++, tandis que les décisions, les calculs et les règles de configuration passent en C#.
7. Les cas où cela ne convient toujours pas
Bien sûr, ce n’est pas une solution universelle. Il existe des cas où cela ne convient clairement pas.
- Vous voulez manipuler directement des classes C++,
std::vectorou des exceptions- Dans ce cas, C++/CLI ou un wrapper côté natif est plus naturel.
- Vous voulez entrer dans le monde de l’enregistrement COM, de l’automatisation VBA / Office ou des extensions Explorer
- Il vaut mieux réfléchir à cela dans le contexte de COM.
- Vous voulez faire le pont entre 32 bits et 64 bits, ou franchir une frontière de processus
- Plutôt qu’une DLL in-process, une configuration COM / IPC / processus séparé est plus judicieuse.
- Vous voulez pouvoir décharger un plugin par la suite
- Mieux vaut ne pas utiliser une bibliothèque partagée Native AOT en prévoyant de la décharger.
- Vos bibliothèques dépendantes reposent fortement sur la réflexion ou la génération de code dynamique
- Si des avertissements apparaissent lors de la publication AOT, il est plus sûr de ne pas les ignorer à la légère.
En fin de compte, la ligne de partage est la capacité ou non à se contenter d’une ABI C. Si ce n’est pas possible, un autre pont sera plus propre.
8. Pièges
Pour finir, voici un résumé des points discrètement faciles à heurter avec les exports Native AOT.
- Les méthodes marquées
UnmanagedCallersOnlydoivent êtrestatic. - Elles ne peuvent pas se trouver dans une méthode générique ni dans une classe générique.
- Si vous voulez un export nommé, ajoutez
EntryPoint. - Évitez
ref/in/out; privilégiez le retour via des arguments pointeurs. - Seules les méthodes de l’assembly cible de la publication sont exportées. Mettre l’attribut sur une méthode d’une bibliothèque référencée ne suffit pas à la faire apparaître telle quelle.
- La bitness de l’appelant et de la DLL doivent correspondre.
- Les avertissements de publication comptent beaucoup. Si des avertissements AOT / trimming apparaissent, il est plus sûr de les traiter en premier.
Chacun de ces points, une fois qu’on le connaît, relève de l’évidence. Mais si on le découvre sans le savoir à l’avance, on y passe des heures plutôt pénibles.
9. Résumé
Quand on veut appeler C# depuis C/C++, ce qui vient d’abord à l’esprit, c’est COM, C++/CLI, ou un processus séparé. Ce sont tous des choix valables.
Cependant, si vous voulez insérer un traitement C# comme une DLL native in-process, Native AOT + UnmanagedCallersOnly est une option vraiment intéressante.
Reprenons les points clés une dernière fois.
- Ne pas exposer C# tel quel, mais l’aplatir en une ABI C
- Expliciter la gestion de la durée de vie sur la base de handles
- Franchir la frontière avec des codes d’erreur, et non des exceptions
- Fixer la convention d’appel
- Garder les méthodes exportées minces, séparées de la logique interne
Ce qui est fait n’a rien de spectaculaire. Mais la façon de découper cette frontière a un impact réel sur la maintenabilité par la suite. Quand vous voulez tirer parti de vos actifs natifs tout en apportant la productivité de C# à la seule couche logique, cette configuration mérite d’être gardée en mémoire.
10. Références
- L’ensemble du code d’exemple de cet article (bibliothèque C#, exemple d’appel en C++, tests unitaires) - komurasoft-blog-samples (GitHub)
- Native code interop with Native AOT - Microsoft Learn
- Building native libraries - Microsoft Learn
- Native AOT deployment - Microsoft Learn
- UnmanagedCallersOnlyAttribute Class - Microsoft Learn
- UnmanagedCallersOnlyAttribute.CallConvs Field - Microsoft Learn
- C# compiler breaking changes: ref / ref readonly / in / out are not allowed on methods attributed with UnmanagedCallersOnly
- Building Native Libraries with NativeAOT - dotnet/samples
- Pourquoi un wrapper C++/CLI est un choix efficace pour utiliser des DLL natives depuis C# - KomuraSoft Blog
- Comment appeler une DLL 64 bits depuis une application 32 bits - Étude de cas où un pont COM aide - KomuraSoft Blog
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Les applications métier fonctionnent-elles sous Windows on Arm ? — La réalité de l'émulation x64 (Prism) et des DLL/COM natifs
Une réponse, destinée aux développeurs et aux services informatiques, à la question « notre application métier fonctionnera-t-elle sous W...
Appeler les API Win32 en toute sécurité depuis C# — Guide pratique de P/Invoke (DllImport / LibraryImport / CsWin32)
Un tour d'horizon pratique des points essentiels pour appeler des API Win32 depuis C# via P/Invoke : différences entre DllImport et Libra...
Compatibilité descendante des interfaces DLL et COM — Tableau de décision : quels changements cassent les appelants
Quels changements apportés à une DLL ou à un composant COM cassent réellement leurs appelants ? Nous détaillons les trois niveaux de comp...
Versionner le schéma de base de données d'une application métier — pratiques de migration pour éviter que « chaque client ait une base différente »
Guide pratique pour versionner le schéma de base de données d'applications métier dont les bases sont dispersées chez chaque client. Impl...
CI/CD pratique pour les applications WinForms / WPF — Automatiser du build à la signature et à la distribution avec GitHub Actions
Guide pratique pour mettre en place le CI/CD des applications WinForms / WPF avec GitHub Actions. Couvre un YAML minimal de build+tests s...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Interopérabilité 32 bits / 64 bits
Compatibilité 32/64 bits, limites natives et choix de conception Windows.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Comme il s'agit d'implémenter la frontière entre C# et C/C++, ce thème s'inscrit directement dans le conseil en conception et en implémentation au titre du développement d'applications Windows.
Réutilisation et migration d'actifs existants
Sur la question de la manière de construire un pont entre des actifs natifs existants et .NET, ce thème s'accorde aussi bien avec notre accompagnement en réutilisation et migration des actifs existants.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Peut-on appeler du code C# depuis C++ ?
- Oui. Avec Native AOT de .NET, vous pouvez publier une bibliothèque de classes C# comme bibliothèque partagée native, et exposer les méthodes marquées UnmanagedCallersOnly comme points d'entrée C. Autrement dit, vous pouvez utiliser C# comme « la DLL native appelée » depuis C/C++, en in-process.
- Dans quels cas cette configuration est-elle adaptée ?
- Dans les cas où l'on veut conserver l'application native telle quelle tout en déplaçant vers C# uniquement des parties comme la logique de décision, le traitement de chaînes, l'interprétation de la configuration ou les règles de calcul. Sa caractéristique est que « le côté natif reste le protagoniste, et C# n'est appelé que comme un composant » — l'inverse de P/Invoke ou de C++/CLI. À l'inverse, si vous appelez des fonctions C depuis C#, P/Invoke est adapté ; si vous voulez manipuler naturellement des types et une gestion de propriété C++, C++/CLI est adapté ; et si vous devez franchir la frontière 32 bits / 64 bits ou une frontière de processus, COM / IPC est adapté.
- À quoi faut-il faire attention lors de la conception de l'API ?
- Ce qui est exporté n'est jamais qu'un point d'entrée de fonction C, donc il ne faut pas exposer directement string, List<T> ou des exceptions à la frontière. L'essentiel est de la ramener à une API C plate du type create / destroy / operate, d'expliciter la gestion de la durée de vie et les codes d'erreur, de traiter les chaînes sous forme de pointeur + longueur + capacité de buffer, de ne jamais laisser les exceptions franchir la frontière, et de fixer la convention d'appel. Le point clé est de concevoir la surface de la frontière comme une ABI C, et non comme du .NET.
- Existe-t-il un exemple de code fonctionnel ?
- Oui. Un ensemble d'exemples complet, buildable et exécutable — comprenant la bibliothèque C# publiée avec Native AOT, un exemple d'appel en C++ et des tests unitaires — est publié dans le dépôt GitHub komurasoft-blog-samples. Les exemples de code partent du principe d'une DLL Windows, mais le raisonnement reste quasiment le même sous Linux / macOS.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics