مقدمة
النظر في التعليمات البرمجية التالية:
توقيع أسلوب
Marshal.FinalReleaseComObject كما يلي:
public static int FinalReleaseComObject(Object o)
نقوم بإنشاء كائن COM بسيط ، ونقوم ببعض الأعمال ، وننشره على الفور. يبدو أن ما يمكن أن يحدث الخطأ؟ نعم ، إن إنشاء كائن داخل حلقة لا نهائية ليس ممارسة جيدة ، لكن
GC سيضطلع بجميع الأعمال القذرة. الواقع مختلف بعض الشيء:

لفهم سبب تسرب الذاكرة ، تحتاج إلى فهم كيفية عمل
ديناميكية . يوجد بالفعل العديد من المقالات حول هذا الموضوع حول حبري ، على سبيل المثال
هذا المقال ، لكنها لا تتناول تفاصيل التنفيذ ، لذلك سنقوم بإجراء بحثنا الخاص.
أولاً ، سوف ندرس بالتفصيل آلية العمل
الديناميكية ، ثم سنقوم بتقليل المعرفة المكتسبة إلى صورة واحدة وفي النهاية سنناقش أسباب هذا التسرب وكيفية تجنبه. قبل الغوص في الكود ، دعونا نوضح البيانات المصدر: أي مجموعة من العوامل تؤدي إلى التسرب؟
التجارب
ربما يكون إنشاء العديد من كائنات
COM الأصلية فكرة سيئة بحد ذاته؟ لنفحص:
كل شيء جيد هذه المرة:

دعنا نرجع إلى الإصدار الأصلي من الكود ، ولكن نغير نوع الكائن:
ومرة أخرى ، لا مفاجآت:

لنجرب الخيار الثالث:
حسنًا ، الآن يجب أن نحصل على نفس السلوك! نعم؟ لا :(

ستكون صورة مماثلة إذا قمت بتعريف com
ككائن أو إذا كنت تعمل مع
Managed COM . تلخيص النتائج التجريبية:
- لا يؤدي إنشاء كائنات COM أصلية بمفرده إلى حدوث تسرب - GC تتكيف بنجاح مع مسح الذاكرة
- عند العمل مع أي فئة مدارة ، لا تحدث تسربات
- عندما تقوم بصياغة كائن إلى كائن صريح ، كل شيء على ما يرام أيضًا
التطلع إلى المستقبل ، إلى النقطة الأولى ، يمكننا إضافة حقيقة مفادها أن العمل مع كائنات
ديناميكية (أساليب الاتصال أو العمل مع الخصائص) في حد ذاته لا يؤدي أيضًا إلى حدوث تسربات. الاستنتاج يقترح نفسه: يحدث تسرب للذاكرة عند تمرير كائن
حيوي (بدون تحويل نوع "يدوي") يحتوي على
COM أصلي ، كمعلمة أسلوب.
نحن بحاجة للذهاب أعمق
حان الوقت لنتذكر
ما تدور حوله هذه
الديناميكية :
مرجع سريعيوفر C # 4.0 نوعًا جديدًا من الديناميكي . يتجنب هذا النوع التحقق من النوع الثابت بواسطة المترجم. في معظم الحالات ، يعمل كنوع كائن . في وقت التحويل البرمجي ، من المفترض أن عنصرًا تم إعلانه ديناميكيًا يدعم أي عملية. هذا يعني أنك لست بحاجة إلى التفكير في مصدر العنصر - من COM API أو لغة ديناميكية مثل IronPython أو باستخدام الانعكاس أو من أي مكان آخر. علاوة على ذلك ، إذا كانت الشفرة غير صالحة ، فسيتم طرح الأخطاء في وقت التشغيل.
على سبيل المثال ، إذا كان الأسلوب exampleMethod1 في التعليمة البرمجية التالية يحتوي على معلمة واحدة تمامًا ، فسوف يتعرف المترجم على أن الاستدعاء الأول لطريقة ec.exampleMethod1 (10 ، 4) غير صالح لأنه يحتوي على معلمتين. سيؤدي هذا إلى خطأ ترجمة. لم يتم التحقق من استدعاء الطريقة الثانية ، dynamic_ec.exampleMethod1 (10 ، 4) بواسطة المحول البرمجي ، حيث يتم تعريف dynamic_ec على أنه ديناميكي ، لذلك. لن يكون هناك أخطاء تجميع. ومع ذلك ، لن يمر الخطأ دون أن يلاحظه أحد إلى الأبد - سيتم اكتشافه في وقت التشغيل.
static void Main(string[] args) { ExampleClass ec = new ExampleClass();
class ExampleClass { public ExampleClass() { } public ExampleClass(int v) { } public void exampleMethod1(int i) { } public void exampleMethod2(string str) { } }
تخضع التعليمات البرمجية التي تستخدم المتغيرات
الحيوية تغييرات كبيرة أثناء التحويل البرمجي. هذا الكود:
dynamic com = Activator.CreateInstance(comType); Marshal.FinalReleaseComObject(com);
يتحول إلى ما يلي:
object instance = Activator.CreateInstance(typeFromClsid);
حيث
o__0 هي الفئة الثابتة المولدة ، و
p__0 هو الحقل الثابت فيها:
private class o__0 { public static CallSite<Action<CallSite, Type, object>> p__0; }
ملاحظة: لكل تفاعل مع ديناميكي ، يتم إنشاء حقل CallSite. هذا ، كما سنرى لاحقًا ، ضروري لتحسين الأداء.لاحظ أنه لم يتبق أي ذكر
للديناميكية - يتم تخزين كائننا الآن في متغير
كائن type. دعنا نسير عبر الكود الذي تم إنشاؤه. أولاً ، يتم إنشاء الربط الذي يصف ماذا وماذا نفعل؟
Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "FinalReleaseComObject", (IEnumerable<Type>) null, typeof (Foo), (IEnumerable<CSharpArgumentInfo>) new CSharpArgumentInfo[2] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.IsStaticType, (string) null), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, (string) null) })
هذا هو وصف لعملية ديناميكية لدينا. اسمحوا لي أن أذكرك بأننا نمرر متغير
ديناميكي إلى طريقة
FinalReleaseComObject .
- CSharpBinderFlags.ResultDiscarded - لا يتم استخدام نتيجة تنفيذ الطريقة في المستقبل
- "FinalReleaseComObject" - اسم الأسلوب يسمى
- typeof (Foo) - سياق العملية ؛ نوع المكالمة
CSharpArgumentInfo - وصف معلمات الربط. في حالتنا:
- CSharpArgumentInfo.Create (CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.IsStaticType، (string) null) - وصف المعلمة الأولى - فئة Marshal: إنه ثابت ويجب اعتبار نوعه عند الربط
- CSharpArgumentInfo.Create (CSharpArgumentInfoFlags.None ، (سلسلة) خالية) - وصف لمعلمة الطريقة ، عادة لا توجد معلومات إضافية.
إذا لم يكن الأمر يتعلق باستدعاء طريقة ، ولكن ، على سبيل المثال ، استدعاء خاصية من كائن
حيوي ، فسيكون هناك
CSharpArgumentInfo واحد فقط يصف الكائن
الديناميكي نفسه.
CallSite هو التفاف على تعبير ديناميكي. يحتوي على مجالين مهمين بالنسبة لنا:
- تي العام التحديث
- الهدف العام
من الشفرة التي تم إنشاؤها ، يتضح أنه عند إجراء أي عملية ، يتم
استهداف الهدف باستخدام معلمات تصفه:
Foo.o__0.p__0.Target((CallSite) Foo.o__0.p__0, typeof (Marshal), instance);
بالاقتران مع
CSharpArgumentInfo الموضح أعلاه
، يعني هذا الرمز ما يلي: تحتاج إلى استدعاء الأسلوب FinalReleaseComObject في فئة Marshal الثابتة مع معلمة المثيل. في وقت المكالمة الأولى ، يتم تخزين نفس المفوض في
الهدف كما هو الحال في
التحديث . مفوض
التحديث مسؤول عن مهمتين مهمتين:
- ربط العملية الديناميكية بعملية ثابتة (آلية المتعهدة نفسها خارج نطاق هذه المقالة)
- تشكيل ذاكرة التخزين المؤقت
نحن مهتمون بالنقطة الثانية. تجدر الإشارة هنا إلى أنه عند العمل مع كائن ديناميكي ، نحتاج إلى التحقق من صحة العملية في كل مرة. هذه مهمة كثيفة الاستخدام للموارد ، لذلك أريد تخزين نتائج عمليات الفحص هذه مؤقتًا. فيما يتعلق باستدعاء طريقة ذات معلمة ، نحتاج إلى تذكر ما يلي:
- النوع الذي تسمى الطريقة
- نوع الكائن الذي تم تمريره بواسطة المعلمة (للتأكد من أنه يمكن نقله إلى نوع المعلمة)
- هل العملية صالحة
بعد ذلك ، عند استدعاء
الهدف مرة أخرى ، لا نحتاج إلى تنفيذ روابط مكلفة نسبيًا: فقط قارن بين الأنواع ، وإذا كانت متطابقة ، فاتصل بالوظيفة الهدف. لحل هذه المشكلة ، يتم إنشاء
ExpressionTree لكل عملية ديناميكية ، والتي تخزن
القيود والدالة الموضوعية التي يرتبط بها التعبير الديناميكي.
هذه الوظيفة يمكن أن تكون من نوعين:
- خطأ ملزم : على سبيل المثال ، يتم استدعاء أسلوب على كائن ديناميكي غير موجود أو لا يمكن تحويل كائن ديناميكي إلى نوع المعلمة التي يتم تمريرها إليها: ثم تحتاج إلى رمي استثناء مثل Microsoft.CSharp.RuntimeBinderException: 'NoSuchMember'
- التحدي قانوني: ثم قم فقط بتنفيذ الإجراء المطلوب
يتم تكوين
ExpressionTree هذا عند تنفيذ مفوض
التحديث وتخزينه في
الهدف .
الهدف - ذاكرة التخزين المؤقت
L0 ، سنتحدث أكثر عن ذاكرة التخزين المؤقت لاحقًا.
لذلك ، يخزن
Target آخر
ExpressionTree الذي تم إنشاؤه من خلال مفوض
التحديث . لنرى كيف تبدو هذه
القاعدة كمثال للنوع
المُدار الذي تم تمريره إلى طريقة
Boo :
public class Foo { public void Test() { var type = typeof(int); dynamic instance = Activator.CreateInstance(type); Boo(instance); } public void Boo(object o) { } }
.Lambda CallSite.Target<System.Action`3[Actionsss.CallSite,ConsoleApp12.Foo,System.Object]>( Actionsss.CallSite $$site, ConsoleApp12.Foo $$arg0, System.Object $$arg1) { .Block() { .If ($$arg0 .TypeEqual ConsoleApp12.Foo && $$arg1 .TypeEqual System.Int32) { .Return #Label1 { .Block() { .Call $$arg0.Boo((System.Object)((System.Int32)$$arg1)); .Default(System.Object) } } } .Else { .Default(System.Void) }; .Block() { .Constant<Actionsss.Ast.Expression>(IIF((($arg0 TypeEqual Foo) AndAlso ($arg1 TypeEqual Int32)), returnUnamedLabel_0 ({ ... }) , default(Void))); .Label .LabelTarget CallSiteBinder.UpdateLabel: }; .Label .If ( .Call Actionsss.CallSiteOps.SetNotMatched($$site) ) { .Default(System.Void) } .Else { .Invoke (((Actionsss.CallSite`1[System.Action`3[Actionsss.CallSite,ConsoleApp12.Foo,System.Object]])$$site).Update)( $$site, $$arg0, $$arg1) } .LabelTarget #Label1: } }
أهم كتلة بالنسبة لنا:
.If ($$arg0 .TypeEqual ConsoleApp12.Foo && $$arg1 .TypeEqual System.Int32)
المعلمتان اللتان يطلق عليهما الهدف "arg arg0" و
$ arg1 :
Foo.o__0.p__0.Target((CallSite) Foo.o__0.p__0, <b>this</b>, <b>instance</b>);
ترجمت إلى إنسان ، وهذا يعني ما يلي:
لقد تحققنا بالفعل من أنه إذا كانت المعلمة الأولى من النوع
Foo والثانية هي
Int32 ، فيمكنك الاتصال بأمان
Boo ((object) $$ arg1) .
.Return #Label1 { .Block() { .Call $$arg0.Boo((System.Object)((System.Int32)$$arg1)); .Default(System.Object) }
ملاحظة: في حالة وجود خطأ ملزم ، تبدو كتلة Label1 على النحو التالي: .Return #Label1 { .Throw .New Microsoft.CSharp.RuntimeBinderException("NoSuchMember")
وتسمى هذه الشيكات
القيود .
هناك نوعان من
القيود : حسب نوع الكائن وحالة الكائن المحددة (يجب أن يكون الكائن هو نفسه تمامًا). إذا فشل أحد القيود على الأقل ، فسيتعين علينا إعادة التحقق من التعبير الديناميكي للتأكد من صحته ، لذلك سنتصل بمندوب
Update . وفقًا للمخطط المعروف لدينا بالفعل ، سوف يقوم بالربط مع الأنواع الجديدة
ويحفظ ExpressionTree الجديد في
الهدف .
مخبأ
لقد اكتشفنا بالفعل أن
الهدف هو
ذاكرة التخزين المؤقت L0 . في كل مرة يتم فيها استدعاء
الهدف ، أول شيء سنفعله هو تجاوز القيود المخزنة بالفعل فيه. إذا فشلت القيود وتم إنشاء رابط جديد ، فستنتقل القاعدة القديمة في نفس الوقت إلى
L1 و
L2 . في المستقبل ، عندما تفتقد ذاكرة التخزين المؤقت
L0 ، سيتم البحث عن القواعد من
L1 و
L2 حتى يتم العثور على واحدة مناسبة.
- L1 : القواعد العشر الأخيرة التي تركت L0 (المخزنة مباشرة في CallSite )
- L2 : آخر 128 قواعد تم إنشاؤها باستخدام مثيل binder معين (وهو CallSiteBinder ، فريد لكل CallSite )
الآن يمكننا أخيرًا إضافة هذه التفاصيل إلى
كلي واحد ووصف في شكل خوارزمية ما يحدث عندما
يتم استدعاء Foo.Bar (someDynamicObject) :
1. يتم إنشاء الموثق الذي يتذكر السياق والأسلوب المسمى على مستوى توقيعاتهم
2. في المرة الأولى التي تسمى العملية ، يتم إنشاء
ExpressionTree ، والذي يقوم بتخزين:
2.1
القيود . في هذه الحالة ، سيكون هذا قيدان على نوع معلمات الربط الحالية
2.2
الوظيفة الموضوعية : إما
استبعاد بعض الاستثناءات (في هذه الحالة يكون من المستحيل ، لأن أي
ديناميكية ستؤدي إلى الاعتراض بنجاح) أو استدعاء الأسلوب
Bar3. قم بتجميع وتنفيذ ExpressionTree الناتج
4. عندما تتذكر العملية ، هناك خياران ممكنان:
عملت 4.1
القيود : فقط اتصل
بار4.2
لم تنجح القيود : كرر الخطوة 2 لمعلمات الربط الجديدة
لذلك ، مع مثال النوع
المُدار ، أصبح من الواضح تقريبًا كيف تعمل
الديناميكية من الداخل. في الحالة الموضحة ، لن
نفقد ذاكرة التخزين المؤقت أبدًا ، نظرًا لأن الأنواع
متماثلة دائمًا * ، لذلك
سيتم استدعاء
التحديث مرة واحدة فقط عند تهيئة
CallSite . ثم ، لكل مكالمة ، سيتم فحص القيود فقط وسيتم استدعاء الوظيفة الهدف على الفور. هذا هو في اتفاق ممتاز مع ملاحظاتنا من الذاكرة: لا يوجد حساب - لا تسرب.
* لهذا السبب ، يقوم المترجم بإنشاء CallSites لكل منها: يتم تقليل احتمال فقدان ذاكرة التخزين المؤقت L0 بشكل كبيرحان الوقت لاكتشاف كيف يختلف هذا المخطط في حالة كائنات
COM الأصلية . دعنا نلقي نظرة على
ExpressionTree :
.Lambda CallSite.Target<System.Action`3[Actionsss.CallSite,ConsoleApp12.Foo,System.Object]>( Actionsss.CallSite $$site, ConsoleApp12.Foo $$arg0, System.Object $$arg1) { .Block() { .If ($$arg0 .TypeEqual ConsoleApp12.Foo && .Block(System.Object $var1) { $var1 = .Constant<System.WeakReference>(System.WeakReference).Target; $var1 != null && (System.Object)$$arg1 == $var1 }) { .Return #Label1 { .Block() { .Call $$arg0.Boo((System.__ComObject)$$arg1); .Default(System.Object) } } } .Else { .Default(System.Void) }; .Block() { .Constant<Actionsss.Ast.Expression>(IIF((($arg0 TypeEqual Foo) AndAlso {var Param_0; ... }), returnUnamedLabel_1 ({ ... }) , default(Void))); .Label .LabelTarget CallSiteBinder.UpdateLabel: }; .Label .If ( .Call Actionsss.CallSiteOps.SetNotMatched($$site) ) { .Default(System.Void) } .Else { .Invoke (((Actionsss.CallSite`1[System.Action`3[Actionsss.CallSite,ConsoleApp12.Foo,System.Object]])$$site).Update)( $$site, $$arg0, $$arg1) } .LabelTarget #Label1: } }
يمكن ملاحظة أن الاختلاف موجود فقط في التقييد الثاني:
.If ($$arg0 .TypeEqual ConsoleApp12.Foo && .Block(System.Object $var1) { $var1 = .Constant<System.WeakReference>(System.WeakReference).Target; $var1 != null && (System.Object)$$arg1 == $var1 })
إذا كان لدينا في حالة الكود
المدار قيودان على نوع الكائنات ، ثم نرى هنا أن القيد الثاني يتحقق من معادلة المثيلات من خلال
WeakReference .
ملاحظة: يتم أيضًا استخدام تقييد المثيل بالإضافة إلى كائنات COM لـ TransparentProxyفي الممارسة العملية ، بناءً على معرفتنا بالتخزين المؤقت ، فهذا يعني أنه في كل مرة نقوم بإعادة إنشاء كائن
COM في حلقة ، سنفتقد ذاكرة التخزين المؤقت
L0 (و
L1 / L2 أيضًا ، لأنه سيتم تخزين القواعد القديمة مع الروابط هناك إلى الحالات القديمة). الافتراض الأول الذي يسألك في الرأس هو أن ذاكرة التخزين المؤقت للقواعد تتدفق. لكن الرمز هناك بسيط للغاية وكل شيء على ما يرام هناك: يتم حذف القواعد القديمة بشكل صحيح. في الوقت نفسه ، لا يؤدي استخدام
WeakReference في
ExpressionTree إلى منع
GC من تجميع كائنات غير ضرورية.
آلية حفظ القواعد في ذاكرة التخزين المؤقت L1: const int MaxRules = 10; internal void AddRule(T newRule) { T[] rules = Rules; if (rules == null) { Rules = new[] { newRule }; return; } T[] temp; if (rules.Length < (MaxRules - 1)) { temp = new T[rules.Length + 1]; Array.Copy(rules, 0, temp, 1, rules.Length); } else { temp = new T[MaxRules]; Array.Copy(rules, 0, temp, 1, MaxRules - 1); } temp[0] = newRule; Rules = temp; }
إذن ما هي الصفقة؟ دعنا نحاول توضيح الفرضية: يحدث تسرب للذاكرة في مكان ما عند ربط كائن
COM .
التجارب ، الجزء 2
مرة أخرى ، دعنا ننتقل من استنتاجات المضاربة إلى التجارب. أولاً ، دعنا نكرر ما يفعله المترجم بالنسبة لنا:
نتحقق من:

تم الحفاظ على التسرب. المعرض. لكن ما السبب؟ بعد دراسة رمز المجلدات (الذي نتركه وراء الأقواس) ، من الواضح أن الشيء الوحيد الذي يؤثر على نوع كائننا هو خيار التقييد. ربما هذه ليست مسألة كائنات
COM ، ولكن الموثق؟ ليس هناك الكثير من الخيارات ، فلنستحث الربط المتعدد للنوع
المُدار :
while (true) { object instance = Activator.CreateInstance(typeof(int)); var autogeneratedBinder = Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "Boo", null, typeof(Foo), new CSharpArgumentInfo[2] { CSharpArgumentInfo.Create( CSharpArgumentInfoFlags.UseCompileTimeType, null), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }); var callSite = CallSite<Action<CallSite, Foo, object>>.Create(autogeneratedBinder); callSite.Target(callSite, this, instance); }

نجاح باهر! يبدو أننا وقعناه. المشكلة ليست على الإطلاق في
كائن COM ، كما بدا لنا في البداية ، فقط بسبب القيود المفروضة على المثيل ، هذه هي الحالة الوحيدة التي يحدث فيها الربط عدة مرات داخل حلقة لدينا. في جميع الحالات الأخرى ، نهضت
ذاكرة التخزين المؤقت L0 وألزم مرة واحدة.
النتائج
تسرب الذاكرة
إذا كنت تعمل مع متغيرات
ديناميكية تحتوي على
COM الأصلي أو
TransparentProxy ، فلا تمررها أبداً كمعلمات أسلوب. إذا كنت لا تزال بحاجة إلى القيام بذلك ، استخدم المدخل الصريح
للاعتراض ، ثم سيتخلف المترجم عنك
خطأ :
dynamic com = Activator.CreateInstance(comType);
صحيح :
dynamic com = Activator.CreateInstance(comType);
كإجراء احترازي إضافي ، حاول إنشاء مثل هذه الكائنات في حالات نادرة قدر الإمكان. الفعلي لجميع إصدارات
.NET Framework . (في الوقت الحالي) ليس مناسبًا جدًا لـ.
NET الأساسية ، لأنه لا
يوجد دعم لكائنات
COM الحيوية .
إنتاجية
من مصلحتك حدوث حالات ندرة في ذاكرة التخزين المؤقت في حالات نادرة قدر الإمكان ، لأنه في هذه الحالة لا توجد حاجة للعثور على قاعدة مناسبة في ذاكرات التخزين المؤقت عالية المستوى. سيحدث
المفقود في ذاكرة التخزين المؤقت
L0 بشكل أساسي في حالة عدم تطابق نوع الكائن
الحيوي مع الاحتفاظ بالقيود.
dynamic com = GetSomeObject(); public object GetSomeObject() {
ومع ذلك ، من الناحية العملية ، من المحتمل ألا تلاحظ الاختلاف في الأداء ما لم يتم قياس عدد المكالمات إلى هذه الوظيفة بالملايين أو إذا كان تباين الأنواع كبيرًا بشكل غير عادي. التكاليف في حالة تفويت على ذاكرة التخزين المؤقت
L0 هي مثل ،
N هو عدد الأنواع:
- أقل من 10 إذا فاتتك ، قم بالتكرار فقط على قواعد ذاكرة التخزين المؤقت L1 الموجودة
- 10 < N <128 . تعداد L1 و L2 ذاكرة التخزين المؤقت (بحد أقصى 10 و N تكرار). إنشاء وملء مجموعة من 10 عناصر
- N > 128. تكرار عبر ذاكرة التخزين المؤقت L1 و L2 . إنشاء وملء صفائف 10 و 128 عنصر. إذا فاتك ذاكرة التخزين المؤقت L2 ، أعد الربط
في الحالتين الثانية والثالثة ، سيزداد الحمل على GC.
استنتاج
لسوء الحظ ، لم نعثر على سبب حقيقي لتسرب الذاكرة ، وهذا سيتطلب دراسة منفصلة للموثق. لحسن الحظ ، يوفر
WinDbg تلميحًا لمزيد من التحقيق: يحدث شيء سيء في
DLR . العمود الأول هو عدد الكائنات

علاوة
لماذا الصب لمنع الكائن صراحة منع تسرب؟يمكن إلقاء أي نوع على
العنصر ، وبالتالي تتوقف العملية عن أن تكون ديناميكية.
لماذا لا توجد تسربات عند العمل مع الحقول وطرق كائن COM؟هذا ما يشبه
ExpressionTree للوصول إلى الحقل:
.If ( .Call System.Dynamic.ComObject.IsComObject($$arg0) ) { .Return #Label1 { .Dynamic GetMember ComMarks(.Call System.Dynamic2.ComObject.ObjectToComObject($$arg0)) } }