ساخت DLLهای بومی ویندوز (WinAPI) با استفاده از NET Native AOT.
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۳۰ ۰۹:۲۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
StdCall) فراهم شدهاست. این قابلیت به توسعهدهندگان اجازه میدهد بدون نیاز به کامپایلر ++C، خروجیهایی تولید کنند که از طریق هر زبان یا محیطی که توانایی فراخوانی توابع WinAPI را دارد (مانند برنامههای سنتی C++/C، پایتون یا اسکریپتهای سیستمی)، قابل استفاده باشند. این مقاله به بررسی نحوه راهاندازی، پیادهسازی و چالشهای این رویکرد میپردازد..csproj) است.csproj<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
<NativeLib>Shared</NativeLib>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>PublishAot: به کامپایلر اعلام میکند که کدها را به صورت پیشفرض به زبان ماشین (Ahead-of-Time) تبدیل کند.NativeLib: با مقداردهی Shared مشخص میکند که خروجی باید یک کتابخانه پویا (.dll در ویندوز) باشد، نه یک فایل اجرایی مستقل.AllowUnsafeBlocks: برای مدیریت اشارهگرها (Pointers) و کدهای ناامن که در تعاملات بومی (P/Invoke) اجتنابناپذیر هستند، الزامی است.static تعریف کرده و با استفاده از ویژگی [UnmanagedCallersOnly] آنها را برای محیط بیرون قابل دسترسی کنیم:using System.Runtime.InteropServices;
public static class NativeExports
{
[UnmanagedCallersOnly(EntryPoint = "AddNumbers", CallConvs = new[] { typeof(CallConvStdcall) })]
public static int AddNumbers(int a, int b)
{
return a + b;
}
}نکته فنی: ویژگیUnmanagedCallersOnlyبه داتنت میگوید که این متد مستقیماً توسط کدهای غیرمدیریتشده (Unmanaged) صدا زده میشود. آرگومانCallConvsنوع قراردارد فراخوانی را رویCallConvStdcall(استاندارد WinAPI) تنظیم میکند که برای سازگاری کامل با کتابخانههای ویندوزی ضروری است.
dotnet build معمولی تنها یک فایل اسمبلی داتنتی استاندارد تولید میکند. برای تولید DLL بومی، باید از دستور dotnet publish به همراه مشخص کردن معماری هدف (Target Runtime) استفاده کنید:# انتشار برای معماری ۶۴ بیتی ویندوز dotnet publish -c Release -r win-x64 # انتشار برای معماری ۳۲ بیتی ویندوز dotnet publish -c Release -r win-x86
int، long، double) مستقیماً جابهجا میشوند، اما برای رشتهها و اشیاء پیچیده، باید از اشارهگرها و فرآیند Marshaling استفاده کرد.[UnmanagedCallersOnly(EntryPoint = "TransformString", CallConvs = new[] { typeof(CallConvStdcall) })]
public static int TransformString(IntPtr inputPtr, IntPtr outputPtr, int outputBufferLength)
{
// تبدیل اشارهگر ورودی به رشته داتنتی
string inputStr = Marshal.PtrToStringAnsi(inputPtr) ?? string.Empty;
if (outputPtr == IntPtr.Zero || outputBufferLength < 1)
return 0; // بافر خروجی معتبر نیست
// انجام عملیات روی رشته
string result = "Processed: " + inputStr;
// تبدیل رشته داتنت به بایتهای ANSI
byte[] resultBytes = System.Text.Encoding.ASCII.GetBytes(result);
int bytesToCopy = Math.Min(resultBytes.Length, outputBufferLength - 1);
// کپی کردن دادهها در بافر خروجی
Marshal.Copy(resultBytes, 0, outputPtr, bytesToCopy);
// قرار دادن کاراکتر انتهای رشته (Null-terminator)
Marshal.WriteByte(outputPtr, bytesToCopy, 0);
return bytesToCopy; // تعداد بایتهای نوشتهشده
}LayoutKind.Sequential) است.[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Ansi)]
public struct EmployeeInfo
{
public int Id;
public double Salary;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string Name;
}
public static class StructureProcessor
{
[UnmanagedCallersOnly(EntryPoint = "ProcessEmployee", CallConvs = new[] { typeof(CallConvStdcall) })]
public static int ProcessEmployee(IntPtr structPtr)
{
if (structPtr == IntPtr.Zero)
return -1;
// خواندن ساختار از اشارهگر حافظه
var employee = Marshal.PtrToStructure<EmployeeInfo>(structPtr);
// اعمال تغییرات بر روی دادهها
employee.Id += 1000;
employee.Salary += 500.50;
employee.Name = "Updated: " + employee.Name;
// بازنویسی ساختار تغییر یافته در همان آدرس حافظه
Marshal.StructureToPtr(employee, structPtr, false);
return 0; // موفقیتآمیز
}
}#include <windows.h>
#include <iostream>
// تعریف ساختار مطابق با چیدمان داتنت
#pragma pack(push, 1)
struct EmployeeInfo {
int Id;
double Salary;
char Name[64];
};
#pragma pack(pop)
// تعریف امضای تابع موجود در DLL
typedef int(__stdcall* ProcessEmployeeFunc)(EmployeeInfo*);
int main() {
// بارگذاری کتابخانه تولید شده توسط داتنت
HMODULE hDll = LoadLibrary(TEXT("DotNetNativeLib.dll"));
if (!hDll) {
std::cout << "خطا در بارگذاری DLL" << std::endl;
return 1;
}
// یافتن آدرس تابع
ProcessEmployeeFunc ProcessEmployee = (ProcessEmployeeFunc)GetProcAddress(hDll, "ProcessEmployee");
if (ProcessEmployee) {
// مقداردهی اولیه ساختار
EmployeeInfo emp = { 1, 2500.0, "Ali" };
// فراخوانی تابع
ProcessEmployee(&emp);
// چاپ نتایج تغییر یافته توسط داتنت
std::cout << "New ID: " << emp.Id << "\n";
std::cout << "New Salary: " << emp.Salary << "\n";
std::cout << "New Name: " << emp.Name << std::endl;
}
FreeLibrary(hDll);
return 0;
}Reflection، سیستمهای تزریق وابستگی مفرط (Heavy DI) یا سریالسازیهای مبتنی بر رفلکشن (مثل نسخه قدیمی NewtonSoft.Json) یا کار نخواهند کرد یا خروجی را به شدت حجیم میکنند. باید همواره از Source Generatorها استفاده شود.IntPtr از برنامه کلاینت گرفته میشود، خارج از لایه نظارت GC است و مدیریت یا آزادسازی آن (Memory Management) همچنان بر عهده برنامه فراخوان خواهد بود..NET Native AOT جهت ساخت کتابخانههای WinAPI، یک راهکار بسیار عالی اما محدود به بازارهای هدف خاص (Niche Feature) است. این رویکرد برای توسعهدهندگانی که تمایل دارند فرآیند کامپایلر سنگین ++C، را از فرآیند توسعه (Build Pipeline) خود حذف کنند یا برای سناریوهای راهاندازی اولیه سیستم (Bootstrapping) بدون نیاز به نصب پیشنیازهای حجیم، گزینهای بینظیر است. با این وجود، اگر محیط فراخوان شما توانایی پشتیبانی از COM Interop را دارد، استفاده از رویکردهای سنتی تعامل داتنت احتمالاً رابطهای برنامهنویسی (API) بسیار تمیزتری را نسبت به کار با اشارهگرهای سختِ سیستمی در اختیار شما قرار میدهد. در نهایت، Native AOT ابزاری قدرتمند است که شکاف میان سادگی توسعه #C و کارایی و بینیازی محیطهای Native را به شکل چشمگیری پر کردهاست.