فصل ۲۴ ـ ادامه: Shared Memory و COM Interoperability
ادامهٔ Mapping یک Struct روی Unmanaged Memory
پس از تعریف MySharedData، برنامهٔ نخست block مشترک را با اندازهٔ دقیق struct میسازد، pointer ریشه را به MySharedData* تبدیل میکند و fieldها را مستقیماً در shared memory مینویسد:
static unsafe void Main()
{
using (SharedMem sm = new SharedMem ("MyShare", false,
(uint) sizeof (MySharedData)))
{
void* root = sm.Root.ToPointer();
MySharedData* data = (MySharedData*) root;
data->Value = 123;
data->Letter = 'X';
data->Numbers[10] = 1.45f;
Console.WriteLine ("Written to shared memory");
Console.ReadLine();
Console.WriteLine ("Value is " + data->Value);
Console.WriteLine ("Letter is " + data->Letter);
Console.WriteLine ("11th Number is " + data->Numbers[10]);
Console.ReadLine();
}
}
using (MemoryMappedFile mmFile =
MemoryMappedFile.CreateNew ("MyShare", 1000))
using (MemoryMappedViewAccessor accessor =
mmFile.CreateViewAccessor())
{
byte* pointer = null;
accessor.SafeMemoryMappedViewHandle.AcquirePointer (ref pointer);
void* root = pointer;
...
}
برنامهٔ دوم، در حالی که برنامهٔ اول داخل ReadLine منتظر است، با همان نام به shared memory متصل میشود، مقدارهای موجود را میخواند و سپس آنها را تغییر میدهد:
static unsafe void Main()
{
using (SharedMem sm = new SharedMem ("MyShare", true,
(uint) sizeof (MySharedData)))
{
void* root = sm.Root.ToPointer();
MySharedData* data = (MySharedData*) root;
Console.WriteLine ("Value is " + data->Value);
Console.WriteLine ("Letter is " + data->Letter);
Console.WriteLine ("11th Number is " + data->Numbers[10]);
data->Value++;
data->Letter = '!';
data->Numbers[10] = 987.5f;
Console.WriteLine ("Updated shared memory");
Console.ReadLine();
}
}
خروجی دو برنامه نشان میدهد برنامهٔ دوم ابتدا Value=123، Letter='X' و عدد یازدهم 1.45 را میخواند، سپس آنها را به 124، ! و 987.5 تغییر میدهد و برنامهٔ اول همان مقادیر جدید را میبیند.
Pointerها در این مثال نسبتاً سادهاند، اما مثال از جهت دیگری هم «unsafe» است: دو process ممکن است همزمان به همان حافظه دسترسی داشته باشند. در application واقعی باید برای fieldهایی مانند Value و Letter در صورت نیاز volatile در نظر گرفت تا JIT یا hardware آنها را نامناسب cache نکند؛ و با پیچیدهترشدن تعامل، احتمالاً access باید با یک cross-process Mutex محافظت شود، همانگونه که در multithreaded code از lock استفاده میشود.
fixed و بلوک fixed {...}
یکی از محدودیتهای mapping مستقیم struct این است که struct فقط میتواند unmanaged type داشته باشد. برای share کردن string باید مثلاً fixed character array داشته باشیم و تبدیل به/از string را دستی انجام دهیم:
[StructLayout (LayoutKind.Sequential)]
unsafe struct MySharedData
{
// ...
const int MessageSize = 200;
fixed char message [MessageSize];
public string Message
{
get { fixed (char* cp = message) return new string (cp); }
set
{
fixed (char* cp = message)
{
int i = 0;
for (; i < value.Length && i < MessageSize - 1; i++)
cp [i] = value [i];
cp [i] = '\0';
}
}
}
}
اولین کاربرد keywordِ fixed، 200 کاراکتر یا 400 byte را inline داخل struct رزرو میکند. کاربرد دوم در property معنای دیگری دارد: object را pin میکند تا اگر garbage collection داخل block رخ داد، CLR struct زیربنایی را در heap جابهجا نکند، چون محتوا با direct pointer پیمایش میشود. حتی اگر این struct فعلاً در unmanaged memory باشد، compiler باید امکان استفادهٔ managed را در نظر بگیرد؛ مثلاً object obj = new MySharedData(); آن را box کرده و روی heap قرار میدهد.
این مثال نحوهٔ نمایش string در struct mapشده به unmanaged memory را نشان میدهد. برای typeهای پیچیدهتر میتوان از serialization استفاده کرد، مشروط بر اینکه serialized data هرگز از space رزروشدهٔ struct بیشتر نشود؛ در غیر این صورت با fieldهای بعدی بهطور ناخواسته overlap خواهد کرد.
COM Interoperability
Runtimeِ .NET پشتیبانی ویژهای از COM دارد و اجازه میدهد COM objectها از .NET و objectهای .NET از دنیای COM استفاده شوند. COM فقط در Windows در دسترس است.
هدف COM
COM مخفف Component Object Model است؛ استاندارد binary برای تعامل با libraryها که Microsoft در 1993 منتشر کرد. انگیزهٔ اصلی، ارتباط componentها بهصورت مستقل از زبان و مقاومتر در برابر تغییر نسخه بود. پیش از COM، DLLهای Windows معمولاً function و structهای C را publish میکردند؛ رویکردی هم language-specific و هم شکننده، چون specification یک type از implementation آن جدا نبود و حتی افزودن field جدید به struct میتوانست compatibility را بشکند.
COM با «COM interface» specification نوع را از implementation جدا کرد و علاوه بر procedure call ساده، method call روی objectهای stateful را ممکن ساخت. مدل برنامهنویسی .NET را میتوان تکامل همین اصول دانست: توسعهٔ cross-language و تکامل componentهای binary بدون شکستن applicationهای وابسته.
مبانی COM Type System
Type system در COM حول interfaceها میچرخد. COM interface شبیه .NET interface است، اما ضروریتر است چون COM type functionality خود را فقط از طریق interface expose میکند. در .NET یک class مانند Foo را میتوان مستقیم مصرف کرد و implementation متد را بدون recompilation caller تغییر داد؛ حتی overload جدید افزود. در COM برای همین decoupling، Foo از طریق interface مانند IFoo expose میشود.
public interface IFoo { string Test(); }
Interface منتشرشده در COM immutable در نظر گرفته میشود و COM method overloading را نیز پشتیبانی نمیکند. بنابراین برای افزودن version جدیدِ functionality، interface قبلی تغییر نمیکند و interface دیگری مانند زیر اضافه میشود:
public interface IFoo2 { string Test (string s); }
پشتیبانی از چند interface برای versionability کتابخانههای COM حیاتی است.
IUnknown و IDispatch
هر COM interface با GUID شناسایی میشود. Root interface در COM، IUnknown است و تمام COM objectها باید آن را پیادهسازی کنند. سه method آن عبارتاند از AddRef، Release و QueryInterface. دو مورد نخست برای lifetime management بر پایهٔ reference counting هستند؛ COM برای unmanaged code طراحی شده و garbage collection خودکار ندارد. QueryInterface در صورت پشتیبانی object، referenceای با interface درخواستی برمیگرداند.
برای dynamic programming، scripting و automation، COM object میتواند IDispatch را نیز پیادهسازی کند تا dynamic languageها بتوانند late-bound آن را فراخوانی کنند؛ چیزی شبیه dynamic در C# اما برای invocationهای ساده.
فراخوانی COM Component از C#
بهسبب پشتیبانی داخلی CLR، معمولاً مستقیماً با IUnknown یا IDispatch کار نمیکنید. با CLR objectها کار میکنید و runtime فراخوانیها را از طریق Runtime-Callable Wrapper یا RCW به COM marshal میکند. Runtime همچنین AddRef/Release و تبدیل primitive typeها را مدیریت میکند.
برای static typing به proxy typeهایی نیاز است که هر COM member را به memberِ .NET تبدیل کنند. اینها COM interop type هستند. ابزار tlbimp.exe بر اساس type library یک COM component، interop typeها را تولید و در COM interop assembly compile میکند. اگر component چند interface داشته باشد، ابزار یک type واحد با union memberهای interfaceها میسازد.
در Visual Studio میتوان از Add Reference و تب COM کتابخانهای مانند Microsoft Excel Object Library را افزود. نمونه:
using System;
using Excel = Microsoft.Office.Interop.Excel;
var excel = new Excel.Application();
excel.Visible = true;
excel.WindowState = Excel.XlWindowState.xlMaximized;
Excel.Workbook workBook = excel.Workbooks.Add();
((Excel.Range)excel.Cells[1, 1]).Font.FontStyle = "Bold";
((Excel.Range)excel.Cells[1, 1]).Value2 = "Hello World";
workBook.SaveAs (@"d:\temp.xlsx");
Excel.Application یک COM interop type است که runtime type آن RCW است؛ propertyهایی مانند Workbooks و Cells نیز interop typeهای بیشتری برمیگردانند.
Optional Parameter و Named Argument
چون COM APIها overload ندارند، methodهای بسیار پارامتری با optional parameter رایجاند. شکل قدیمی فراخوانی SaveAs نیاز داشت جای هر optional argument، System.Reflection.Missing.Value داده شود. C# از optional parameterهای COM آگاه است، بنابراین کافی است:
workBook.SaveAs (@"d:\temp.xlsx");
Compiler همان فرم کامل را expand میکند. Named argument نیز اجازه میدهد parameter اضافی را مستقل از position مشخص کنید:
workBook.SaveAs (@"d:\test.xlsx", Password:"foo");
پارامترهای implicit ref
برخی COM APIها ــ بهخصوص Microsoft Word ــ تقریباً همهٔ parameterها را pass-by-reference تعریف کردهاند، حتی وقتی value را تغییر نمیدهند. در گذشته C# مجبور بود keywordِ ref را برای همه argumentها بدهد و این optional parameterها را دشوار میکرد. با implicit ref parameterها در COM call میتوان modifier را حذف کرد:
word.Open ("foo.doc");
هشدار آن است که اگر COM method واقعاً argument را mutate کند، نه compile-time و نه runtime error بابت حذف ref دریافت نخواهید کرد.
Indexerها
حذف ref یک فایدهٔ دیگر دارد: COM indexerهایی که parameterهای ref دارند با syntax معمول C# قابل دسترسی میشوند. همچنین COM property میتواند argument بپذیرد:
myComObject.Foo [123] = "Hello";
در خود C# نمیتوان چنین propertyای نوشت؛ C# فقط default indexer روی خود type را expose میکند. برای تقلید این syntax، Foo باید object دیگری برگرداند که خودش default indexer دارد.
Dynamic Binding
Dynamic binding به دو شکل در COM مفید است. نخست، دسترسی به COM component بدون COM interop type. با Type.GetTypeFromProgID type را میگیرید، با Activator.CreateInstance instance میسازید و سپس با dynamic memberها را صدا میزنید:
Type excelAppType = Type.GetTypeFromProgID ("Excel.Application", true);
dynamic excel = Activator.CreateInstance (excelAppType);
excel.Visible = true;
dynamic wb = excel.Workbooks.Add();
excel.Cells [1, 1].Value2 = "foo";
در این روش IntelliSense و compile-time checking ندارید. Reflection نیز همان کار را بسیار دستوپاگیرتر انجام میدهد. variation دیگری برای componentهایی است که فقط IDispatch دارند، هرچند نادرند.
کاربرد دوم dynamic با COM typeِ variant است که تقریباً معادل object در .NET است. وقتی Embed Interop Types فعال باشد، runtime بهطور پیشفرض variant را به dynamic map میکند و نیاز به cast کمتر میشود:
excel.Cells [1, 1].Font.FontStyle = "Bold";
در مقابل نسخهٔ قدیمی:
var range = (Excel.Range) excel.Cells [1, 1];
range.Font.FontStyle = "Bold";
ضعف روش dynamic از دستدادن autocompletion است؛ بنابراین اغلب بهتر است result را به interop type شناختهشده assign کنیم:
Excel.Range range = excel.Cells [1, 1];
range.Font.FontStyle = "Bold";
Embedding Interop Types
در گذشته تنها راه، reference کردن کامل interop assembly بود؛ مشکلی که برای COM componentهای پیچیده باعث assemblyهای بسیار بزرگ میشد. اکنون میتوان فقط بخشهای مورد استفاده را embed کرد. Compiler assembly را تحلیل میکند، type و memberهای واقعاً لازم را پیدا میکند و definition همانها را مستقیماً در application قرار میدهد؛ در نتیجه هم bloat کمتر میشود و هم فایل اضافی لازم نیست.
برای فعالسازی، در Visual Studio روی COM reference مقدار Embed Interop Types=true بگذارید یا .csproj را ویرایش کنید.
Type Equivalence
CLR برای linked interop typeها type equivalence دارد. اگر دو assembly هر یک به interop typeای link شوند که همان COM type را wrap میکند، typeها equivalent محسوب میشوند؛ حتی اگر interop assemblyهایشان مستقل تولید شده باشند. این قابلیت به TypeIdentifierAttribute در System.Runtime.InteropServices تکیه میکند و compiler هنگام link کردن interop assembly آن را خودکار اعمال میکند. COM typeهایی با GUID یکسان equivalent هستند.
Expose کردن C# Object به COM
مسیر معکوس نیز ممکن است: classهای C# را میتوان برای COM قابل مصرف کرد. CLR با proxyای به نام COM-Callable Wrapper یا CCW این کار را انجام میدهد. CCW typeها را بین دو دنیا marshal میکند، IUnknown و در صورت نیاز IDispatch را پیادهسازی میکند و lifetime آن از سمت COM با reference counting کنترل میشود.
برای expose کردن public class بهعنوان in-proc server، ابتدا interface با GUID یکتا تعریف، آن را COM-visible کنید و interface type را مشخص کنید:
namespace MyCom
{
[ComVisible(true)]
[Guid ("226E5561-C68E-4B2B-BD28-25103ABCA3B1")]
[InterfaceType (ComInterfaceType.InterfaceIsIUnknown)]
public interface IServer
{
int Fibonacci();
}
}
سپس implementation را با GUID یکتای دیگر ارائه دهید:
namespace MyCom
{
[ComVisible(true)]
[Guid ("09E01FCD-9970-4DB3-B537-0EC555967DD9")]
public class Server
{
public ulong Fibonacci (ulong whichTerm)
{
if (whichTerm < 1) throw new ArgumentException ("...");
ulong a = 0;
ulong b = 1;
for (ulong i = 0; i < whichTerm; i++)
{
ulong tmp = a;
a = b;
b = tmp + b;
}
return a;
}
}
}
در project file قابلیت COM hosting را فعال کنید:
<PropertyGroup>
<EnableComHosting>true</EnableComHosting>
</PropertyGroup>
Build یک فایل اضافی مانند MyCom.comhost.dll تولید میکند که برای COM interop ثبت میشود. این فایل بسته به configuration پروژه 32-bit یا 64-bit است و در این سناریو «Any CPU» وجود ندارد. از elevated command prompt میتوان regsvr32 MyCom.comhost.dll را اجرا کرد.
سپس یک زبان COM-capable مانند VBScript میتواند component را مصرف کند:
REM Save file as ComClient.vbs
Dim obj
Set obj = CreateObject("MyCom.Server")
result = obj.Fibonacci(12)
Wscript.Echo result
فعالکردن Registry-Free COM
COM سنتی type information را در registry قرار میدهد. Registry-free COM بهجای registry از manifest برای object activation استفاده میکند. در .csproj این قابلیت را فعال کنید:
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<EnableComHosting>true</EnableComHosting>
<EnableRegFreeCom>true</EnableRegFreeCom>
</PropertyGroup>
Build در این حالت فایلی مانند MyCom.X.manifest تولید میکند.