كيف نستدعي DLL من C# Native AOT من C/C++
· آخر تحديث: · 小村 豪 · C#, .NET, Native AOT, C++, تطوير Windows, تكامل أصيل
سجل التعديلات (3 تحديثات، آخر تحديث 3 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240819)
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621347)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
小村 豪 (2026). كيف نستدعي DLL من C# Native AOT من C/C++. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621347 https://comcomponent.com/ar/blog/2026/03/12/003-csharp-native-aot-native-dll-from-c-cpp/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621347
- DOI (هذه النسخة)
- 10.5281/zenodo.22279748
في المقالة السابقة لماذا يكون غلاف C++/CLI خياراً قويّاً عند استخدام native DLL من C# نظّمنا سطح الحدّ عند استدعاء C++ من C#. هذه المرّة نعكس الاتّجاه: استدعاء C# من C/C++.
ثمّة مواضع تريدون فيها استدعاء معالجة مكتوبة بـ C# من تطبيق C/C++ قائم، لكنّ اتّجاه P/Invoke معكوس، وإدخال C++/CLI أو COM قد يبدو أثقل من اللازم. خصوصاً عندما تريدون الإبقاء على التطبيق الأصيل كما هو، ونقل أجزاء مثل منطق القرار ومعالجة السلاسل وتفسير الإعدادات وقواعد الحساب فقط إلى C#.
يمكن لـ COM أيضاً أن يبني جسراً، لكنّ هذه المرّة أسلوب أكثر in-process وأكثر شبهاً بـ DLL. Native AOT في .NET يصدر مكتبة الأصناف كمكتبة مشتركة أصلية، ويكشف الميثودات الموسومة بـ UnmanagedCallersOnly كنقاط دخول C. أي يمكن استخدام C# بوصفه «native DLL في جهة الاستدعاء».
غير أنّه ليس كلّ شيء يعبر كما هو. تسريب string أو List<T> أو الاستثناءات أو الملكيّة إلى الحدّ يفسد الجوّ فجأة. تنظّم هذه المقالة بمثال أدنى على Windows + C++ متى ينفع هذا الترتيب، وأيّ شكل API يصعب كسره. الفكرة شبه نفسها على Linux / macOS، لكنّ أمثلة الشيفرة تفترض DLL على Windows.
flowchart TB
accTitle: فرق الاتّجاه بين المقالة السابقة وهذه
accDescr: المقالة السابقة حديث سطح الحدّ عند استدعاء native DLL من C#، وهذه تعكس الاتّجاه: استدعاء native DLL من C# الصادر بـ Native AOT in-process من تطبيق C/C++.
prev["السابقة: C# يستدعي C++"] --> wrap["حديث غلاف C++/CLI"]
now["هذه: C/C++ يستدعي C#"] --> aot["تحويل C# إلى DLL بـ Native AOT"]
aot --> entry["UnmanagedCallersOnly هو المدخل"]
الشكل 1: هذه المرّة الاتّجاه معكوس لـ P/Invoke و C++/CLI، ويصير C# «native DLL في جهة الاستدعاء».
كذلك، الشيفرة الواردة في هذه المقالة منشورة على GitHub كمجموعة عيّنات قابلة للبناء والتشغيل (مكتبة C# تُصدَر بـ Native AOT، ومثال استدعاء من C++، واختبارات وحدات).
csharp-native-aot-native-dll-from-c-cpp - komurasoft-blog-samples (GitHub)
المحتويات
- الخلاصة أوّلاً (بجملة)
- دليل الاختيار الذي يُنظر إليه أوّلاً
- مخطّط البنية
- التكوين الأدنى
- 4.1. مشروع C#
- 4.2. شيفرة C# المراد تصديرها
- 4.3. أمر الإصدار
- 4.4. مثال الاستدعاء من جانب C++
- 4.5. التحقّق من أنّ التصدير موجود
- 4.6. عند الرغبة في الربط الساكن بـ import lib
- شكل API يصعب كسره
- 5.1. التقريب إلى C ABI
- 5.2. معاملة السلاسل بمؤشّر + طول + سعة المخزن المؤقّت
- 5.3. عدم عبور الاستثناءات للحدّ
- 5.4. تثبيت اتّفاقيّة الاستدعاء
- 5.5. إبقاء ميثودات الـ Export رقيقة ووضع الجسم في موضع آخر
- الحالات التي تلائمه
- الحالات التي لا تلائمه رغم ذلك
- مواضع التعثّر
- الخلاصة
- المراجع
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 22، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
1. الخلاصة أوّلاً (بجملة)
- إن أردتم استدعاء معالجة C# من C/C++ in-process، فـ Native AOT +
UnmanagedCallersOnlyخيار قويّ جدّاً. - غير أنّ ما يُصدَّر يبقى مدخل دالّة C فحسب. ليس عالماً تُظهر فيه
stringأوList<T>كما هي. - في العمل اليوميّ أستقرّ التسطيح إلى C API مسطّح مثل
create/destroy/operate، وإعلان إدارة العمر وأكواد الخطأ. - إن أردتم التعامل الطبيعي مع أصناف C++ أو STL فيلائم C++/CLI، وإن أردتم التسجيل أو الأتمتة أو العبور بين العمليّات فيلائم COM.
بمعنى آخر: يمكن استخدام C# كمحتوى native DLL، لكنّ سطح الحدّ يُصمَّم كـ C ABI لا كـ .NET. إن أمكنكم القطع هنا، صار سلاحاً ممتعاً جدّاً.
flowchart TB
accTitle: تصميم سطح الحدّ كـ C ABI
accDescr: محتوى C# يمكن أن يبقى أصنافاً ومجموعات كما هو، لكنّ السطح الظاهر للخارج ليس string أو List(T) بل C API مسطّح مثل create / destroy / operate مع إعلان إدارة العمر وأكواد الخطأ.
inner["المحتوى C# عاديّ"] --> face["السطح الظاهر C API مسطّح"]
face --> h["إدارة العمر صريحة بالمقبض"]
face --> e["الخطأ يُعاد كوداً"]
face -.-> ng["لا تُظهر string أو List(T)"]
الشكل 2: موضع القطع واحد. ليس «إظهار .NET كما هو» بل إنزال سطح الحدّ إلى C ABI.
2. دليل الاختيار الذي يُنظر إليه أوّلاً
| المراد | المرشّح القويّ | السبب |
|---|---|---|
| استدعاء دوالّ C من C# | P/Invoke | الاتّجاه صريح، وهو الأطبيعيّ |
| التعامل الطبيعي مع مكتبة C++ من C# | C++/CLI | يسهل امتصاص أنواع C++ والملكيّة والاستثناءات و std::wstring في جانب C++ |
| عبور 32bit / 64bit أو حدّ العمليّات | COM / IPC | DLL داخل العمليّة وحده لا يتجاوز ذلك |
| استدعاء منطق C# من C/C++ بوصفه native DLL | Native AOT + UnmanagedCallersOnly |
يمكن تصدير نقطة دخول C بأنفسكم |
هذا الترتيب ينفع في مواضع «الجانب الأصيل هو البطل، و C# يُستدعى كمكوّن». هنا الاتّجاه معكوس تماماً لـ P/Invoke و C++/CLI.
flowchart TB
accTitle: فرق اتّجاه البطل
accDescr: P/Invoke و C++/CLI اتّجاه C# هو البطل ويستدعي الجانب الأصيل، أمّا ترتيب Native AOT في هذه المقالة فاتّجاه معكوس: الجانب الأصيل هو البطل ويستدعي منطق C# كمكوّن.
cs["C# هو البطل"] -->|"يستدعي الأصيل"| n1["P/Invoke أو C++/CLI"]
nat["الأصيل هو البطل"] -->|"يستدعي C# كمكوّن"| n2["التصدير بـ Native AOT"]
الشكل 3: الجسر يُختار بالاتّجاه. ترتيب هذه المقالة اتّجاه «الأصيل بطل و C# مكوّن».
3. مخطّط البنية
flowchart LR
Cpp["تطبيق C / C++"] -->|استدعاء دالّة cdecl| Dll["DLL من C# صادر بـ Native AOT"]
Dll --> Exports["تصدير موسوم بـ UnmanagedCallersOnly"]
Exports --> Core["منطق الأعمال في C#"]
Exports --> Store["جدول مقابض / إدارة الحالة"]
الشكل 4: من تطبيق C/C++ لا يظهر كدالّة C سوى تصدير UnmanagedCallersOnly.
المظهر بسيط. المهمّ هو محاذاة سطح الحدّ مع دوالّ C. التنفيذ الداخليّ في جانب C# يمكن أن يكون أصنافاً أو مجموعات أو LINQ، لكنّ السطح الظاهر للخارج يُسطَّح.
4. التكوين الأدنى
نجعل هنا مثالاً أدنى يصنع «جامعاً» من جانب C++ ويجمع قيماً ثمّ يجلب المجموع في النهاية. في العمل اليوميّ يمكن أن يكون محرّك قرار أو تفسير إعدادات أو محلّلاً بسيطاً. اعتبروا الشكل أنّ الجانب الأصيل يحمل مقبضاً ويستدعي دوالّ التشغيل بالترتيب.
4.1. مشروع C#
نجهّز أوّلاً مكتبة أصناف.
<!-- 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>
النقطتان اثنتان.
- تفعيل نشر Native AOT
- السماح بـ
unsafeلأنّ وسائط المؤشّر مستخدمة
عيّنة هذه المقالة تفترض net8.0، لكنّ الفكرة نفسها في .NET 9 / 10 أيضاً.
4.2. شيفرة C# المراد تصديرها
الميثود الموسومة بـ UnmanagedCallersOnly تصير المدخل الظاهر من الجانب الأصيل. هنا نُصدر المقبض كعدد صحيح، وندير الحالة الداخليّة بقاموس في جانب 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);
}
}
ما يُفعل ساذج إلى حدّ بعيد.
- ما يُظهر للجانب الأصيل مقبض
intptr_tفقط - جسم الحالة يحمله جانب C#
- تفكيك create / add / get / destroy إلى دوالّ مسطّحة
- قيمة الإرجاع كود خطأ، والقيمة الخارجة تُعاد بوسيط مؤشّر
بهذا الشكل، حتّى إن استُبدل التنفيذ الداخليّ في جانب C# لاحقاً، يبقى ABI جانب C مستقرّاً جدّاً.
flowchart TB
accTitle: API مسطّح قائم على مقبض
accDescr: create يصدر مقبضاً، ودوالّ التشغيل مثل add و get تُستدعى مع المقبض، و destroy يرتّب. جسم الحالة يحمله جانب C#، وقيمة الإرجاع كود خطأ، والقيمة الخارجة تُعاد بوسيط مؤشّر.
create["create: إصدار مقبض"] --> op["add أو get: تشغيل مع المقبض"]
op --> destroy["destroy: الترتيب"]
op -.-> state["جسم الحالة يحمله جانب C#"]
op -.-> err["قيمة الإرجاع كود خطأ"]
الشكل 5: ما يُظهر للجانب الأصيل المقبض ودوالّ التشغيل فقط. استبدال التنفيذ الداخليّ يُبقي ABI مستقرّاً.
حول ترقيم المقبض ملاحظة واحدة. في العيّنة يُحمل عدّاد الترقيم كـ long، وتُحوَّل نتيجة Interlocked.Increment إلى nint. هنا خاصّيّتان يجدر معرفتها.
- 0 لا يُصدر. العدّاد يبدأ من 0، و
Incrementيعيد القيمة بعد الجمع، لذا أوّل مقبض هو 1. لهذا يمكن لجانب C++ استخدامintptr_t handle = 0;علامة على «لم يُحمَل بعد». - في 32bit يحدث قطع.
nintبعرض المؤشّر، ففي 64bit يكون 64bit، وفي بناء 32bit يكون 32bit. التحويل منlongإلىnintيسقط البتّات العليا بصمت، لذا عندما يتجاوز الترقيم 2^32 تدور القيمة. في استخدام يكرّر create / destroy 24 ساعة متواصلة، يمكن الوصول نظريّاً.
ماذا يحدث عند الدوران، اضبطوا هذا بدقّة. استثناء المفتاح المكرّر في s_instances.Add(handle, instance) لا يعين إلا عندما يكون مقبض بالقيمة نفسها «حيّاً الآن». الاستخدام العاديّ لهذا الـ API تكرار create و destroy، والمقبض بعد destroy يختفي من القاموس. أي عند العودة إلى القيمة نفسها بعد الدوران، لا يوجد المفتاح في القاموس فينجح Add. النتيجة أنّ المقبض القديم الذي ما يزال جانب C يحمله يشير إلى نسخة جديدة غير ذات صلة. لا استثناء ولا كود خطأ، فتنكسر القيمة وحدها بهدوء.
أمر آخر: قيمة 2^32 تماماً بتّاتها الـ 32 الدنيا كلّها 0، لذا يُصدر 0 الذي كان يُفترض أنّه علامة «لم يُحمَل بعد».
لذلك لا تعتمدوا فحص المفتاح المكرّر كجهاز أمان. إن أمكن التعامل مع 32bit، فاختاروا واحداً ممّا يلي.
- طمر رقم الجيل في المقبض. الأدنى تسلسل، والأعلى جيل، ويُقدَّم الجيل عند كلّ destroy. حتّى إن عاد التسلسل نفسه لا تتطابق القيمة
- الفشل الدائم عند النفاد. إذا بلغ الترقيم الحدّ الأعلى تُخطئ create بعد ذلك. في جهاز يعمل باستمرار يلزم إعادة التشغيل، لكنّه أسهل تعاملاً من الكسر الهادئ
في الحالين يُحمَل عدّاد الترقيم نفسه كـ nint كي لا يتجاوز عرض nint، ويُحافَظ معاً على عدم إصدار 0.
flowchart TB
accTitle: طريقة الكسر عند دوران ترقيم المقبض والإجراء المضادّ
accDescr: عند دوران الترقيم في 32bit ينجح Add لقيمة اختفت من القاموس بعد destroy، فيشير المقبض القديم الذي ما يزال جانب C يحمله إلى نسخة جديدة غير ذات صلة وينكسر بهدوء. الإجراء طمر رقم الجيل أو الفشل الدائم بعد بلوغ الحدّ.
wrapd["دوران الترقيم في 32bit"] --> add["نجاح Add للقيمة نفسها"]
add --> alias["المقبض القديم يشير إلى نسخة جديدة"]
alias --> silent["لا استثناء ولا كود خطأ، وينكسر"]
silent --> g1["الإجراء: طمر رقم الجيل"]
silent --> g2["الإجراء: الفشل عند النفاد"]
الشكل 6: استثناء المفتاح المكرّر لا يصير جهاز أمان. كسر الدوران هادئ، والإجراء يُزرع في التصميم.
4.3. أمر الإصدار
افتراض واحد مسبقاً. نشر Native AOT يحتاج سلسلة أدوات أصلية على حدة.
إن ضربتم dotnet publish بمجرّد إلحاق PublishAot، فالسقوط ليس في تجميع C# بل في مرحلة الربط الأصيل الأخيرة. هذا أوّل حاجز.
| البيئة | المطلوب |
|---|---|
| Windows | Visual Studio 2022 وما بعده. إدخال حمل عمل «تطوير سطح المكتب بـ C++» مع كلّ المكوّنات الافتراضيّة |
| Ubuntu 18.04 وما بعده | sudo apt-get install clang zlib1g-dev |
| Alpine 3.15 وما بعده | sudo apk add clang build-base zlib-dev |
| Fedora 39 وما بعده / RHEL 8 وما بعده | sudo dnf install clang zlib-ng-devel zlib-ng-compat-devel zlib-devel |
| macOS | Command Line Tools لـ Xcode (مدعوم من .NET 8) |
هذه المقالة تفترض Windows + C++، لذا الحديث عمليّاً «تحقّقوا أوّلاً أنّ حمل عمل C++ في Visual Studio مثبَّت».
أخطاء من نوع عدم العثور على الرابط أو الفشل حول link.exe غالباً هنا.
بعد ذلك تُنشر كمكتبة مشتركة.
dotnet publish -r win-x64 -c Release /p:NativeLib=Shared
بهذا يخرج native DLL تحت bin/Release/net8.0/win-x64/publish/. في مثال Windows .dll، وفي Linux .so، وفي macOS .dylib.
المهمّ هو النشر لكلّ RID. ما صُنع بـ win-x64 لا يُستخدم بافتراض win-arm64، ويلزم أيضاً مطابقة bitness جهة الاستدعاء و DLL.
flowchart TB
accTitle: أوّل حاجز في النشر
accDescr: نشر Native AOT يحتاج سلسلة أدوات أصلية على حدة، وضرب dotnet publish بدونها يسقط في مرحلة الربط الأصيل الأخيرة لا في تجميع C#. يُنشر لكلّ RID وتُطابق bitness.
pub["تشغيل dotnet publish"] --> q{"هل سلسلة الأدوات الأصلية موجودة"}
q -->|"لا"| fail["السقوط في الربط الأصيل الأخير"]
q -->|"نعم"| out["خروج native DLL لكلّ RID"]
out -.-> match["مطابقة bitness مع جهة الاستدعاء"]
الشكل 7: السقوط ليس تجميع C# بل مرحلة الربط. أوّل حاجز وجود سلسلة الأدوات.
4.4. مثال الاستدعاء من جانب C++
هذه المرّة نستبعد حديث import lib مؤقّتاً، ونستدعي بصراحة بـ LoadLibrary / GetProcAddress. بهذا الشكل يسهل رؤية ما صُدِّر وبأيّ توقيع ينبغي الاستلام.
/* 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;
// Native AOT の共有ライブラリはアンロード前提では使わない。
// FreeLibrary(module);
return EXIT_SUCCESS;
}
في هذا المثال، ما يظهر من جانب C++ هو «C API يمكن استدعاؤه بمؤشّر دالّة» فقط. أنّ الداخل مكتوب بـ C# لا يكاد يحتاج وعياً.
بوضع DLL المنشور في المجلّد نفسه مع main.exe وتشغيله، يُجمع 10 و 20، فيكون الخرج القياسيّ هذا فقط.
total = 30
عند الفشل في المنتصف يظهر في جانب std::cerr في أيّ مرحلة سقط. LoadLibraryW failed يعني أنّ DLL لم يُعثر عليه أصلاً، و GetProcAddress failed: km_accumulator_add يعني أنّ DLL مقروء لكنّ التصدير لم يُعثر عليه، هذا الفصل.
flowchart TB
accTitle: الفصل عندما يتعذّر الاستدعاء
accDescr: فشل LoadLibraryW يعني أنّ DLL لم يُحمَّل فيُشتبه بالمسار و bitness ونقص DLL المعتمدة، وفشل GetProcAddress يعني أنّ DLL مقروء لكنّ التصدير لم يُعثر عليه، ونجاح الاثنين يعني إمكان الاستدعاء كـ C API.
s1{"هل نجح LoadLibraryW؟"}
s1 -->|"فشل"| f1["DLL لم يُحمَّل"]
f1 -.-> f1a["الاشتباه بالمسار و bitness و DLL المعتمدة"]
s1 -->|"نجاح"| s2{"هل نجح GetProcAddress؟"}
s2 -->|"فشل"| f2["التصدير لم يُعثر عليه"]
s2 -->|"نجاح"| ok["يمكن الاستدعاء كـ C API"]
الشكل 8: المرحلة التي يسقط فيها تفصل مشكلة تحميل DLL عن مشكلة التصدير.
4.5. التحقّق من أنّ التصدير موجود
عند «تعذّر الاستدعاء»، انظروا أوّلاً هل ظهر الاسم حقّاً في جانب DLL. استخدام dumpbin من Developer Command Prompt في Visual Studio سريع.
dumpbin /exports NativeAotSample.dll
إن اصطفّت الأسماء الأربعة km_accumulator_create / km_accumulator_add / km_accumulator_get_total / km_accumulator_destroy في قائمة name، فنشر جانب C# نجح. لتضييق الاسم:
dumpbin /exports NativeAotSample.dll | findstr km_
إن لم يظهر الاسم هنا فمشكلة جانب C#، وإن ظهر وفشل GetProcAddress فمشكلة جهة الاستدعاء، يمكن الفصل هكذا.
المواضع التي تُنظر عندما يعيد GetProcAddress القيمة NULL بهذا الترتيب تقريباً.
- هل ظهر الاسم في
dumpbin /exports(إن لم يظهر فحديث جانب C#) - هل السلسلة المكتوبة في
EntryPointتطابق تماماً السلسلة الممرَّرة إلىGetProcAddress(حساسية حالة الأحرف أيضاً) - هل تطابق bitness EXE جهة الاستدعاء و DLL
- هل الميثود الموسومة بـ
UnmanagedCallersOnlyمن نوعstaticوليست داخل generic - هل كُتبت السمة في جانب التجميعة المستهدفة بالنشر (كتابتها في مكتبة مُشار إليها لا تُظهرها على السطح)
لاحظوا أنّ فشل LoadLibraryW نفسه ليس حديث تصدير. اشتبهوا أوّلاً بمسار DLL و bitness ونقص DLL المعتمدة.
flowchart TB
accTitle: ترتيب التحقّق عندما يكون GetProcAddress هو NULL
accDescr: التحقّق بالترتيب: هل ظهر الاسم في قائمة exports لـ dumpbin، وهل تطابق سلسلة EntryPoint الممرَّرة تماماً، وهل تطابق bitness، وهل الميثود static خارج generic، وهل كُتبت السمة في جانب التجميعة المستهدفة بالنشر.
c1["هل ظهر الاسم في dumpbin"] --> c2["هل السلسلة تطابق تماماً"]
c2 --> c3["هل تطابق bitness"]
c3 --> c4["هل هي static خارج generic"]
c4 --> c5["هل كُتبت السمة في جانب النشر"]
c1 -.->|"لم يظهر"| cs["التحقيق كمشكلة جانب C#"]
الشكل 9: النظر أوّلاً إلى ظهور الاسم في التصدير يحسم مشكلة جانب C# أم جهة الاستدعاء.
4.6. عند الرغبة في الربط الساكن بـ import lib
حتى هنا كتبنا بأسلوب LoadLibrary / GetProcAddress. لأنّه يسهل رؤية ما صُدِّر وبأيّ توقيع ينبغي الاستلام.
في العمل اليوميّ كثيراً ما تريدون «تضمين ترويسة واستدعاء الدالّة كما هي». عندئذٍ يصير تحميلاً ساكناً بمكتبة استيراد. الخطوات:
- إن خرجت مكتبة استيراد (
.lib) في مخرجات النشر، اربطوها - إن لم تخرج، جهّزوا ملفّ
.defيرتّب أسماء التصدير، واصنعوا مكتبة الاستيراد بـlib.exe /def:NativeAotSample.def /out:NativeAotSample.lib /machine:x64 - في جانب الترويسة استخدموا إعلان دالّة عاديّاً لا نوع مؤشّر دالّة
/* native_api_static.h */
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
int __cdecl km_accumulator_create(intptr_t* out_handle);
int __cdecl km_accumulator_add(intptr_t handle, int value);
int __cdecl km_accumulator_get_total(intptr_t handle, int64_t* out_total);
int __cdecl km_accumulator_destroy(intptr_t handle);
#ifdef __cplusplus
}
#endif
بهذا الشكل تصير شيفرة جهة الاستدعاء صريحة جدّاً. بالمقابل، إن لم يُعثر على DLL يسقط عند تشغيل العمليّة، فيصعب تشغيل من نوع «الجسم يعمل وهذه الوظيفة وحدها غير متاحة». إن أردتم الإدخال كإضافة، فأسلوب LoadLibrary كما هو أسهل تعاملاً.
flowchart TB
accTitle: الاختيار بين التحميل الديناميكيّ والربط الساكن
accDescr: أسلوب LoadLibrary يحمّل في وقت التشغيل فيلائم الإدخال كإضافة، والربط الساكن بمكتبة استيراد يتيح الاستدعاء كما هو بعد تضمين الترويسة مقابل السقوط عند تشغيل العمليّة إن غاب DLL.
q{"كيف تريدون الإدماج"}
q -->|"إدخال كإضافة"| dyn["أسلوب LoadLibrary"]
q -->|"الاستدعاء كما هو بالترويسة"| stat["ربط ساكن بـ import lib"]
stat -.-> risk["غياب DLL يسقط عند التشغيل"]
الشكل 10: الكتابة الصريحة للربط الساكن، أمّا التشغيل بـ «هذه الوظيفة وحدها غائبة» فللتحميل الديناميكيّ.
لاحظوا أنّ الإصدار كمكتبة ساكنة (NativeLib=Static) غير مدعوم رسميّاً، لذا الأأمن عدم الاعتماد عليه.
5. شكل API يصعب كسره
إمكان التصدير بـ Native AOT ممتع، لكنّ في العمل اليوميّ ما لا يُصدَّر أهمّ.
5.1. التقريب إلى C ABI
نرتّب الكلمات أوّلاً. جوهر هذه المقالة «تصميم سطح الحدّ كـ C ABI لا كـ .NET»، و ABI اختصار Application Binary Interface، وهو اتفاق كيف تتلاقى الثنائيّات المجمَّعة في وقت التشغيل. اعتبروه اتّفاقاً على مستوى لغة الآلة لا مستوى الشيفرة المصدر. محتواه أساساً ثلاثة.
| الاتّفاق | ماذا يقرّر | ماذا يحدث إن لم يُحفَظ هنا |
|---|---|---|
| اتّفاقيّة الاستدعاء (calling convention) | كيف تُمرَّر الوسائط في السجلات والمكدّس، وأين تُوضع قيمة الإرجاع، ومن يعيد المكدّس بعد الاستدعاء: المستدعي أم المستدعى | انزياح الوسائط، وكسر المكدّس مباشرة بعد العودة |
| تخطيط النوع | كم بايت لكلّ نوع، وفي أيّ موضع تصطفّ أعضاء struct (الحشو والمحاذاة) | تعذّر قراءة القيمة من منتصف البنية |
| الاسم والربط | تهجئة اسم الدالّة المصدَّرة، ووجود الزخرفة (decoration) من عدمه | تعذّر عثور GetProcAddress على الاسم |
cdecl و stdcall اسما العنصر الأوّل «اتّفاقيّة الاستدعاء» من بين هذه. أصناف C++ والاستثناءات تختلف فيها هذه الاتّفاقات الثلاثة حسب المصرِّف، لذا إخراجها كما هي إلى الحدّ لا يتلاقى. بالمقابل، التضييق على دوالّ C والأنواع الأساسيّة يجعل الاتّفاق بسيطاً فيسهل التلاقي. «التقريب إلى C ABI» يعني إنزال سطح الحدّ إلى نطاق هذا الاتّفاق البسيط.
flowchart TB
accTitle: محتوى اتّفاق اسمه ABI
accDescr: ABI اتّفاق كيف تتلاقى الثنائيّات المجمَّعة في وقت التشغيل، ويتكوّن من ثلاثة: اتّفاقيّة الاستدعاء التي تقرّر تمرير الوسائط وقيمة الإرجاع، وتخطيط النوع، واسم التصدير والزخرفة.
abi["ABI (اتّفاق مستوى لغة الآلة)"] --> a1["اتّفاقيّة الاستدعاء"]
abi --> a2["تخطيط النوع"]
abi --> a3["الاسم والربط"]
a1 -.-> ex["cdecl و stdcall اسما هذا الموضع"]
الشكل 11: «التقريب إلى C ABI» إنزال سطح الحدّ إلى نطاق تكون فيه هذه الاتّفاقات الثلاثة بسيطة.
بعد ذلك، الأنواع الخارجة إلى الحدّ أهدأ إن قُرِّبت من البداية إلى هذا النطاق.
- أنواع أساسيّة مثل
int32_t/int64_t/double - struct بتخطيط ثابت
- مقبض يعادل
intptr_t/void* uint8_t*وطول
بالمقابل، ما لا تريدون تسريبه إلى الخارج من البداية هذا النطاق.
stringobjectList<T>TaskSpan<T>- أصناف C++ أو
std::vectorأوstd::wstring
محاولة عبور هذه كما هي تعكّر سطح الحدّ سريعاً. المهمّ عدم تسريب ملاءمات C# إلى C++، وعدم تسريب ملاءمات C++ إلى C# أكثر من اللازم.
flowchart TB
accTitle: أنواع تُخرج إلى الحدّ وأنواع لا تُسرَّب
accDescr: ما يُخرج إلى الحدّ يُقرَّب إلى الأنواع الأساسيّة و struct بتخطيط ثابت والمقبض والمؤشّر والطول، ولا تُسرَّب string و object و List(T) و Task وأصناف C++ و STL إلى الخارج.
edge["أنواع تُخرج إلى الحدّ"] --> ok1["أنواع أساسيّة و struct ثابت ومقبض"]
edge --> ok2["مؤشّر وطول"]
keepx["أنواع لا تُسرَّب إلى الخارج"] --> ng1["string و List(T) و Task"]
keepx --> ng2["أصناف C++ و STL"]
الشكل 12: المهمّ في العمل اليوميّ «ما لا يُصدَّر». لا تسرّبوا ملاءمات الطرفين إلى الحدّ.
لتقليل أخطاء النسخ، نضع أيضاً جدول التقابل. توقيع الميثود الموسومة بـ UnmanagedCallersOnly لا يقبل إلا أنواعاً blittable، لذا ينحصر عمليّاً في هذا النطاق.
| جانب C# | جانب C / C++ | ملاحظة |
|---|---|---|
byte / sbyte |
uint8_t / int8_t |
|
short / ushort |
int16_t / uint16_t |
|
int / uint |
int32_t / uint32_t |
|
long / ulong |
int64_t / uint64_t |
long في C++ 32bit على Windows و 64bit على LP64 في Linux، لذا الأأمن عدم كتابته long واستخدام int64_t |
nint / nuint |
intptr_t / uintptr_t |
عرض المؤشّر. في بناء 32bit يصير 32bit |
float / double |
float / double |
|
bool |
لا يُستخدم | ليس blittable. مرّروا 0 / 1 بـ int32_t |
char / string |
لا يُستخدم | السلاسل كما في 5.2 تُعامل بمؤشّر + طول |
T* (مؤشّر unsafe) |
T* |
القيمة الخارجة تُعاد بهذا |
| struct بتخطيط ثابت | struct بالتخطيط نفسه | رتّبوا ترتيب الأعضاء والنوع والحشو على الجانبين حتماً |
5.2. معاملة السلاسل بمؤشّر + طول + سعة المخزن المؤقّت
عند الرغبة في تبادل سلاسل، يغري إخراج string فوراً، لكنّ الأفضل الصبر هنا. على حدّ المكتبة يسهل الفهم بالإنزال إلى شكل كهذا مثلاً.
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);
أي تقرير ترميز الأحرف والطول ومن يخصّص المخزن المؤقّت أوّلاً. لأنّه Windows فثمّة خيار التقريب إلى UTF-16، لكنّ النظر إلى لغات أخرى يجعل UTF-8 أسهل تعاملاً في كثير من الحالات.
5.3. عدم عبور الاستثناءات للحدّ
حدّ دالّة أصيلة ليس لطيفاً كثيراً كتعبير عن الاستثناءات. على الأقلّ، عدم تصميم تسريب استثناء managed كما هو إلى جهة الاستدعاء أأمن.
في العمل اليوميّ يسهل التعامل بـ:
- قيمة الإرجاع status code
- البيانات الفعليّة مخزن مؤقّت out أو وسيط مؤشّر
- جلب معلومات إضافيّة بشكل
get_last_errorعند الحاجة
لا بريق فيه، لكنّ تصميماً هادئاً كهذا ينفع لاحقاً. أي عدم بدء مصارعة فجأة على سطح الحدّ.
flowchart TB
accTitle: تصميم خطأ لا يعبر فيه الاستثناء الحدّ
accDescr: عدم تسريب استثناء managed كما هو إلى جهة الاستدعاء، وجعل قيمة الإرجاع status code، والبيانات الفعليّة مخزن مؤقّت out أو وسيط مؤشّر، وجلب معلومات إضافيّة بشكل get_last_error عند الحاجة.
exc["استثناء داخل C#"] --> stop["التقاطه داخل الحدّ"]
stop --> code["قيمة الإرجاع status code"]
stop --> outp["البيانات الفعليّة بوسيط مؤشّر"]
stop -.-> last["المعلومات الإضافيّة بشكل get_last_error"]
الشكل 13: لا يُترك الاستثناء يعبر الحدّ، بل يُترجم إلى عالم status code ووسيط مؤشّر ويُعاد.
5.4. تثبيت اتّفاقيّة الاستدعاء
في العيّنة صرّحنا بـ CallConvCdecl. إن أُهمل يصير اتّفاقيّة الاستدعاء الافتراضيّة للمنصّة، لكن إن أردتم تثبيت الترويسة أو نوع مؤشّر الدالّة، التصريح من هنا أصعب وقوعاً في الحادث.
خصوصاً إن أمكن التعامل مع x86، فإبهام هذا يصير شاقاً لاحقاً. على x64 قد لا يظهر على السطح، لكنّ تقرير القاعدة من البداية أفضل.
5.5. إبقاء ميثودات الـ Export رقيقة ووضع الجسم في موضع آخر
الميثود الموسومة بـ UnmanagedCallersOnly ليست بافتراض الاستدعاء كما هي من شيفرة managed عاديّة. لذا البدء بكتابة منطق الأعمال كلّه فيها يصعّب الاختبار أيضاً.
في العيّنة أيضاً وُضعت إدارة الكيان في AccumulatorStore، و NativeExports المصدَّر مدخل رقيق فقط. هذا مهمّ جدّاً.
- ميثود التصدير: نافذة ABI
- الصنف الداخليّ: منطق C# عاديّ
بهذا التقسيم يمكن التفكير في حدّ C++ وشيفرة جسم C# منفصلين.
flowchart TB
accTitle: التصدير رقيق والجسم في موضع آخر
accDescr: إبقاء ميثود التصدير الموسومة بـ UnmanagedCallersOnly نافذة ABI رقيقة، ووضع إدارة الكيان ومنطق الأعمال في صنف داخليّ، يفصل الحدّ عن الجسم ويسهّل الاختبار.
exp["ميثود التصدير (نافذة رقيقة)"] --> core["صنف داخليّ (C# عاديّ)"]
exp -.-> abi["مسؤوليّة فحص ABI والتحويل فقط"]
core -.-> test["قابل للاختبار كـ C# عاديّ"]
الشكل 14: لا تبدأوا كتابة منطق الأعمال في التصدير. تقسيم النافذة والجسم يسهّل الصيانة والاختبار.
6. الحالات التي تلائمه
هذا الترتيب ينسجم براحة كبيرة في مواضع كهذه.
- الإبقاء على تطبيق C/C++ قائم، ونقل جزء من منطق الأعمال فقط إلى C#
- عدم جعل التثبيت المسبق لوقت تشغيل .NET افتراض توزيع
- إمكان الإبقاء على سطح الدوالّ المصدَّرة صغيراً
- احتمال الرغبة لاحقاً في الاستدعاء من لغات أخرى مثل Rust أو Go بالـ C API نفسه
خصوصاً ترتيب الإبقاء على التطبيق الأصيل كما هو، وكتابة طبقة منطق سهلة الاستبدال فقط بـ C# ملائم. القطع: الواجهة وتحكّم الأجهزة يبقيان C++، والقرار والحساب وقواعد الإعدادات C#.
flowchart TB
accTitle: قطع ملائم
accDescr: الإبقاء على الواجهة وتحكّم الأجهزة C++، وكتابة طبقة منطق سهلة الاستبدال فقط مثل القرار والحساب وقواعد الإعدادات بـ C#، والربط بسطح C API صغير.
app["تطبيق C/C++ قائم"] --> keepn["الواجهة وتحكّم الأجهزة يبقيان C++"]
app --> logic["القرار والحساب وقواعد الإعدادات C#"]
logic --> api["الربط بسطح C API صغير"]
api -.-> multi["الاستدعاء من لغات أخرى بالسطح نفسه"]
الشكل 15: الإبقاء على الأصيل بطلاً، وإدخال إنتاجيّة C# في طبقة المنطق سهلة الاستبدال فقط.
7. الحالات التي لا تلائمه رغم ذلك
طبعاً هذا ليس لكلّ شيء. المواضع التي لا تلائمه واضحة أيضاً.
- الرغبة في التعامل مع أصناف C++ أو
std::vectorأو الاستثناءات كما هي- عندئذٍ C++/CLI أو غلاف في الجانب الأصيل أطبيعيّ.
- الدخول في عالم تسجيل COM أو أتمتة VBA / Office أو توسيع Explorer
- هنا الأفضل التفكير في سياق COM.
- الرغبة في جسر 32bit / 64bit، أو عبور حدّ العمليّات
- ليس DLL داخل العمليّة، بل COM / IPC / بنية عمليّة منفصلة أنسب.
- الرغبة في تفريغ إضافة لاحقاً
- الأفضل عدم استخدام المكتبة المشتركة من Native AOT بافتراض التفريغ.
- اعتماد المكتبات المعتمدة بقوّة على reflection أو توليد شيفرة ديناميكيّ
- إن ظهر تحذير نشر AOT، فالأأمن عدم تجاهل ذلك التحذير بتهاون.
في النهاية، إمكان القطع بـ C ABI هو خطّ التقسيم. إن تعذّر القطع، فجسر آخر أصفى.
flowchart TB
accTitle: الحالات التي لا تلائمه والجسر البديل
accDescr: التعامل مع أصناف C++ أو الاستثناءات كما هي يذهب إلى C++/CLI، وعالم تسجيل COM أو الأتمتة إلى COM، وعبور bitness أو حدّ العمليّات إلى COM أو IPC، وإضافة بافتراض التفريغ لا يلائم هذا الترتيب نفسه.
q{"ماذا تطلبون"}
q -->|"أنواع C++ أو الاستثناءات كما هي"| cli["إلى C++/CLI أو غلاف"]
q -->|"عالم التسجيل أو الأتمتة"| com["إلى سياق COM"]
q -->|"عبور bitness أو العمليّات"| ipc["إلى COM أو IPC أو عمليّة منفصلة"]
q -->|"التفريغ لاحقاً"| ng["هذا الترتيب لا يلائم الافتراض"]
الشكل 16: خطّ التقسيم «هل يمكن القطع بـ C ABI». إن تعذّر فالجسر الآخر أصفى.
8. مواضع التعثّر
أخيراً نلخّص النقاط التي يسهل التعثّر فيها بهدوء عند تصدير Native AOT.
- الميثود الموسومة بـ
UnmanagedCallersOnlyيجب أن تكونstatic. - لا يمكن وضعها داخل ميثود generic أو صنف generic.
- إن أردتم تصديراً مسمّى فألحقوا
EntryPoint. - الأفضل عدم استخدام
ref/in/out، والإعادة بوسيط مؤشّر. - ما يُصدَّر هو ميثود جانب التجميعة المستهدفة بالنشر. وضع السمة على ميثود مكتبة مُشار إليها لا يُظهرها على السطح كما هي.
- يلزم مطابقة bitness جهة الاستدعاء و DLL.
- تحذير النشر مهمّ جدّاً. إن ظهر تحذير AOT / trimming فالأأمن ترتيب ذلك أوّلاً.
هذه كلّها حديث «بعد معرفتها، نعم كذلك». لكنّ الوقوع فيها مرّة دون معرفة يمرّر وقتاً عسيراً جدّاً.
9. الخلاصة
عندما تريدون استدعاء C# من C/C++، أوّل ما يخطر هو COM أو C++/CLI أو عمليّة منفصلة. وكلّها خيارات صحيحة.
غير أنّه إن أردتم إدخال معالجة C# كـ native DLL داخل العمليّة، فـ Native AOT + UnmanagedCallersOnly خيار ممتع جدّاً.
نذكر النقاط مرّة أخرى فقط.
- لا تُظهروا C# كما هو، بل سطّحوا إلى C ABI
- أعلنوا إدارة العمر بأسلوب المقبض
- اعبروا الحدّ بكود خطأ لا باستثناء
- ثبّتوا اتّفاقيّة الاستدعاء
- أبقوا ميثود التصدير رقيقة، وافصلوا المنطق الداخليّ
ما يُفعل ليس برّاقاً. لكنّ «كيف يُقطع الحدّ» كهذا ينفع كثيراً في الصيانة لاحقاً. عندما تريدون الاستفادة من أصل أصيل وإدخال إنتاجيّة C# في طبقة المنطق فقط، لا خسارة في تذكّر هذا الترتيب.
flowchart TB
accTitle: خمس قواعد لحدّ يصعب كسره
accDescr: التسطيح إلى C ABI، وإدارة العمر بأسلوب المقبض، والعبور بكود خطأ، وتثبيت اتّفاقيّة الاستدعاء، وإبقاء التصدير رقيقاً وفصله عن المنطق الداخليّ، هذه الخمس تصنع حدّاً يصعب كسره.
goal["native DLL من C# يصعب كسره"] --> p1["التسطيح إلى C ABI"]
goal --> p2["إعلان العمر بالمقبض"]
goal --> p3["العبور بكود خطأ"]
goal --> p4["تثبيت اتّفاقيّة الاستدعاء"]
p1 -.-> p5["التصدير رقيق ومفصول عن الجسم"]
الشكل 17: خمس قواعد الخلاصة. تصميم الحدّ الهادئ هو الأكثر نفعاً للصيانة لاحقاً.
10. مراجع
- مجموعة شيفرة العيّنة لهذه المقالة (مكتبة C#، مثال استدعاء من C++، اختبارات وحدات)، csharp-native-aot-native-dll-from-c-cpp
- Microsoft Learn, 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
- GitHub, Building Native Libraries with NativeAOT - dotnet/samples
- Microsoft Learn, DUMPBIN /EXPORTS
- Microsoft Learn, LIB Reference
- مدوّنة شركة كومورا سوفت, لماذا يكون غلاف C++/CLI خياراً قويّاً عند استخدام native DLL من C#
- مدوّنة شركة كومورا سوفت, دراسة حالة يفيد فيها COM: استدعاء DLL بـ 64bit من تطبيق 32bit
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
لماذا تتعطّل الوسائط ── قواعد وسائط سطر أوامر Windows
في Windows لا توجد مصفوفة وسائط؛ يصل إلى CreateProcess سلسلة واحدة ويتولّى الطرف المستقبل تقسيمها. نشرح قواعد CommandLineToArgvW وCRT و.N...
Time Travel Debugging ── تسجيل الأخطاء التي لا تتكرّر في التطبيقات طويلة التشغيل وإرجاعها
خطأ يظهر مرّة في الشهر لا يترك في تفريغ الانهيار سوى نتيجته. سجّل التنفيذ وأرجعه بـ WinDbg Time Travel Debugging (TTD): TTD.exe والمخزن ا...
ما الذي يبقى بعد موت الأب ── تربية العمليات الابنة في كائن Job
لماذا يبقى مساعد SDK ماسكاً الكاميرا أو منفذ COM بعد إنهاء الواجهة قسراً. كيف تجعل كائن Job شجرة العمليات وحدة واحدة وتصمم عمر العملية ال...
الأنابيب المسمّاة عمليّاً ── IPC القياسيّ في Windows من التصميم إلى الأمان
دليل عمليّ للأنابيب المسمّاة، وسيلة IPC القياسيّة في Windows. يغطّي وضع البايت مقابل وضع الرسالة، وخوادم متعدّدة العملاء، وأمان ACL وانتح...
استخدام WMI/CIM من C# وPowerShell ── دليل عمليّ لاسترجاع معلومات العتاد ومراقبة العمليّات والاستعلام عن بُعد
الإجابة الكلاسيكيّة للحصول على الرقم التسلسليّ للحاسوب، ومراقبة المساحة الحرّة على القرص، وكشف بدء عمليّة هي WMI/CIM. يشرح المقال أوامر C...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
لأن الموضوع هو تنفيذ الحدّ الفاصل بين C# وC/C++، فهو يتّصل مباشرة باستشارة التصميم والتنفيذ ضمن تطوير تطبيقات Windows.
دعم إعادة استخدام الأصول القديمة وترحيلها
من حيث كيفيّة بناء الجسر بين الأصول الأصليّة (native) القائمة وبين .NET، فإنّه يتناسب أيضاً مع دعم إعادة استخدام الأصول القديمة وترحيلها.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- هل يمكن استدعاء شيفرة C# من C++؟
- نعم. Native AOT في .NET يصدر مكتبة أصناف C# كمكتبة مشتركة أصلية، ويكشف الميثودات الموسومة بـ UnmanagedCallersOnly كنقاط دخول C. أي يمكن استخدام C# بوصفه native DLL في جهة الاستدعاء، in-process من C/C++.
- ما المواضع التي يلائمها هذا الترتيب؟
- مواضع تريدون فيها الإبقاء على التطبيق الأصيل كما هو، ونقل أجزاء مثل منطق القرار ومعالجة السلاسل وتفسير الإعدادات وقواعد الحساب فقط إلى C#. السمة أنّ «الجانب الأصيل هو البطل، و C# يُستدعى كمكوّن». بالمقابل، لاستدعاء دوالّ C من C# يلائم P/Invoke، وللتعامل الطبيعي مع أنواع C++ والملكيّة يلائم C++/CLI، ولعبور 32bit/64bit أو حدّ العمليّات يلائم COM/IPC.
- ما الملاحظات عند تصميم الـ API؟
- ما يُصدَّر يبقى مدخل دالّة C فحسب، لذا لا تُظهر string أو List<T> أو الاستثناءات كما هي على الحدّ. سطّحوا إلى C API مسطّح مثل create / destroy / operate، وأعلنوا إدارة العمر وأكواد الخطأ، وعاملوا السلاسل بمؤشّر + طول + سعة المخزن المؤقّت، ولا تدعوا الاستثناءات تعبر الحدّ، وثبّتوا اتّفاقيّة الاستدعاء. النقطة تصميم سطح الحدّ كـ C ABI لا كـ .NET.
- هل ثمّة شيفرة عيّنة تعمل؟
- نعم. في مستودع komurasoft-blog-samples على GitHub مجموعة عيّنات قابلة للبناء والتشغيل تتضمّن مكتبة C# تُصدَر بـ Native AOT، ومثال استدعاء من C++، واختبارات وحدات. أمثلة الشيفرة تفترض DLL على Windows، لكنّ الفكرة شبه نفسها على Linux / macOS.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.