diff options
author | Min Huang <huangmin@microsoft.com> | 2022-04-18 04:55:57 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-18 04:55:57 +0300 |
commit | 93806a9b54abe8c61399d47f9c087c69fc07ef85 (patch) | |
tree | 31fd19aa204a0e98fc71100aa61df3d5353541cd | |
parent | b5f62fcd1677c565fbf47cf8b062c5bba4d3c649 (diff) |
Support .NET language feature: function pointers (#623)
* Support .NET language feature: function pointers
* update
* update
* add test cases
* add test dll
* update
* Use delegate * for other languages
* update
* Fix new member added for function pointers
* update
-rw-r--r-- | external/Test/FunctionPointersTest.dll | bin | 0 -> 6656 bytes | |||
-rw-r--r-- | mdoc/Consts.cs | 2 | ||||
-rw-r--r-- | mdoc/Mono.Documentation/Updater/DocumentationEnumerator.cs | 3 | ||||
-rw-r--r-- | mdoc/Mono.Documentation/Updater/Formatters/CSharpFullMemberFormatter.cs | 28 | ||||
-rw-r--r-- | mdoc/Mono.Documentation/Updater/Formatters/CppFormatters/CppFullMemberFormatter.cs | 2 | ||||
-rw-r--r-- | mdoc/Mono.Documentation/Updater/Formatters/DocTypeFullMemberFormatter.cs | 10 | ||||
-rw-r--r-- | mdoc/Mono.Documentation/Updater/Formatters/FSharpFormatter.cs | 3 | ||||
-rw-r--r-- | mdoc/Mono.Documentation/Updater/Formatters/ILFullMemberFormatter.cs | 7 | ||||
-rw-r--r-- | mdoc/Mono.Documentation/Updater/Formatters/JsFormatter.cs | 5 | ||||
-rw-r--r-- | mdoc/Mono.Documentation/Updater/Formatters/MemberFormatter.cs | 79 | ||||
-rw-r--r-- | mdoc/Mono.Documentation/Updater/Formatters/VBFullMemberFormatter.cs | 2 | ||||
-rw-r--r-- | mdoc/mdoc.Test/FormatterTests.cs | 29 | ||||
-rw-r--r-- | mdoc/mdoc.Test/MDocUpdaterTests.cs | 12 | ||||
-rw-r--r-- | mdoc/mdoc.Test/SampleClasses/FunctionPointers.cs | 31 | ||||
-rw-r--r-- | mdoc/mdoc.Test/mdoc.Test.csproj | 1 |
15 files changed, 196 insertions, 18 deletions
diff --git a/external/Test/FunctionPointersTest.dll b/external/Test/FunctionPointersTest.dll Binary files differnew file mode 100644 index 00000000..579f3cdc --- /dev/null +++ b/external/Test/FunctionPointersTest.dll diff --git a/mdoc/Consts.cs b/mdoc/Consts.cs index 58ea9e65..aba2fd0f 100644 --- a/mdoc/Consts.cs +++ b/mdoc/Consts.cs @@ -49,8 +49,10 @@ namespace Mono.Documentation public const string IsByRefLikeAttribute = "System.Runtime.CompilerServices.IsByRefLikeAttribute"; public const string IsReadOnlyAttribute = "System.Runtime.CompilerServices.IsReadOnlyAttribute"; public const string InAttribute = "System.Runtime.InteropServices.InAttribute"; + public const string OutAttribute = "System.Runtime.InteropServices.OutAttribute"; public const string TupleElementNamesAttribute = "System.Runtime.CompilerServices.TupleElementNamesAttribute"; public const string IsExternalInit = "System.Runtime.CompilerServices.IsExternalInit"; public const string NativeIntegerAttribute = "System.Runtime.CompilerServices.NativeIntegerAttribute"; + public const string CallConvPrefix = "System.Runtime.CompilerServices.CallConv"; } } diff --git a/mdoc/Mono.Documentation/Updater/DocumentationEnumerator.cs b/mdoc/Mono.Documentation/Updater/DocumentationEnumerator.cs index 90f533ef..3b710a98 100644 --- a/mdoc/Mono.Documentation/Updater/DocumentationEnumerator.cs +++ b/mdoc/Mono.Documentation/Updater/DocumentationEnumerator.cs @@ -153,6 +153,9 @@ namespace Mono.Documentation.Updater string xmlMemberType = member.Parameters[i]; + // After we support function pointers, "method" as type should be skipped and not be compared with current function pointer type. + if (xmlMemberType == "method") continue; + // TODO: take into account extension method reftype bool xmlIsRefType = xmlMemberType.Contains ('&'); bool refTypesMatch = isRefType == xmlIsRefType; diff --git a/mdoc/Mono.Documentation/Updater/Formatters/CSharpFullMemberFormatter.cs b/mdoc/Mono.Documentation/Updater/Formatters/CSharpFullMemberFormatter.cs index 06368855..ec625447 100644 --- a/mdoc/Mono.Documentation/Updater/Formatters/CSharpFullMemberFormatter.cs +++ b/mdoc/Mono.Documentation/Updater/Formatters/CSharpFullMemberFormatter.cs @@ -1,6 +1,7 @@ using Mono.Cecil; using Mono.Documentation.Util; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -591,43 +592,51 @@ namespace Mono.Documentation.Updater.Formatters { if (DocUtils.IsExtensionMethod (method)) buf.Append ("this "); - AppendParameter (buf, parameters[0]); + AppendParameter(buf, parameters[0]); for (int i = 1; i < parameters.Count; ++i) { - buf.Append (", "); - AppendParameter (buf, parameters[i]); + buf.Append(", "); + AppendParameter(buf, parameters[i]); } } return buf.Append (end); } - private StringBuilder AppendParameter (StringBuilder buf, ParameterDefinition parameter) + protected override StringBuilder AppendParameter(StringBuilder buf, ParameterDefinition parameter) { TypeReference parameterType = parameter.ParameterType; + var refType = new BitArray(3); if (parameterType is RequiredModifierType requiredModifierType) { + switch(requiredModifierType.ModifierType.FullName) + { + case Consts.InAttribute: refType.Set(0, true); break; + case Consts.OutAttribute: refType.Set(1, true); break; + default: break; + } parameterType = requiredModifierType.ElementType; } - if (parameterType is ByReferenceType byReferenceType) { if (parameter.IsOut) { - buf.Append ("out "); + refType.Set(1, true); } else if(parameter.IsIn && DocUtils.HasCustomAttribute(parameter, Consts.IsReadOnlyAttribute)) { - buf.Append("in "); + refType.Set(0, true); } else { - buf.Append("ref "); + refType.Set(2, true); } parameterType = byReferenceType.ElementType; } + buf.Append(refType.Get(0) ? "in " : (refType.Get(1) ? "out " : (refType.Get(2) ? "ref ": ""))); + if (parameter.HasCustomAttributes) { var isParams = parameter.CustomAttributes.Any (ca => ca.AttributeType.Name == "ParamArrayAttribute"); @@ -639,8 +648,7 @@ namespace Mono.Documentation.Updater.Formatters var isNullableType = context.IsNullable (); buf.Append (GetTypeName (parameterType, context)); buf.Append (GetTypeNullableSymbol (parameter.ParameterType, isNullableType)); - buf.Append (" "); - buf.Append (parameter.Name); + buf.Append (string.IsNullOrEmpty(parameter.Name) ? "" : " " + parameter.Name); if (parameter.HasDefault && parameter.IsOptional && parameter.HasConstant) { diff --git a/mdoc/Mono.Documentation/Updater/Formatters/CppFormatters/CppFullMemberFormatter.cs b/mdoc/Mono.Documentation/Updater/Formatters/CppFormatters/CppFullMemberFormatter.cs index 516c9a93..fbbc9e93 100644 --- a/mdoc/Mono.Documentation/Updater/Formatters/CppFormatters/CppFullMemberFormatter.cs +++ b/mdoc/Mono.Documentation/Updater/Formatters/CppFormatters/CppFullMemberFormatter.cs @@ -715,7 +715,7 @@ namespace Mono.Documentation.Updater.Formatters.CppFormatters return buf.Append (end); } - protected virtual StringBuilder AppendParameter (StringBuilder buf, ParameterDefinition parameter) + protected override StringBuilder AppendParameter (StringBuilder buf, ParameterDefinition parameter) { if (parameter.ParameterType is ByReferenceType) { diff --git a/mdoc/Mono.Documentation/Updater/Formatters/DocTypeFullMemberFormatter.cs b/mdoc/Mono.Documentation/Updater/Formatters/DocTypeFullMemberFormatter.cs index 5c370b5d..69a36d02 100644 --- a/mdoc/Mono.Documentation/Updater/Formatters/DocTypeFullMemberFormatter.cs +++ b/mdoc/Mono.Documentation/Updater/Formatters/DocTypeFullMemberFormatter.cs @@ -1,4 +1,7 @@ -namespace Mono.Documentation.Updater
+using Mono.Cecil;
+using System.Text;
+
+namespace Mono.Documentation.Updater
{
class DocTypeFullMemberFormatter : MemberFormatter
{
@@ -20,5 +23,10 @@ {
get { return "+"; }
}
+
+ protected override StringBuilder AppendParameter(StringBuilder buf, ParameterDefinition parameterDef)
+ {
+ return buf.Append(GetName(parameterDef.ParameterType, useTypeProjection: false, isTypeofOperator: false));
+ }
}
}
\ No newline at end of file diff --git a/mdoc/Mono.Documentation/Updater/Formatters/FSharpFormatter.cs b/mdoc/Mono.Documentation/Updater/Formatters/FSharpFormatter.cs index f810f1dd..f4725a6c 100644 --- a/mdoc/Mono.Documentation/Updater/Formatters/FSharpFormatter.cs +++ b/mdoc/Mono.Documentation/Updater/Formatters/FSharpFormatter.cs @@ -692,7 +692,7 @@ namespace Mono.Documentation.Updater return buf; } - private void AppendParameter(StringBuilder buf, ParameterDefinition parameter) + protected override StringBuilder AppendParameter(StringBuilder buf, ParameterDefinition parameter) { bool isFSharpFunction = IsFSharpFunction(parameter.ParameterType); if (isFSharpFunction) @@ -701,6 +701,7 @@ namespace Mono.Documentation.Updater buf.Append(typeName); if (isFSharpFunction) buf.Append(")"); + return buf; } protected override string GetPropertyDeclaration(PropertyDefinition property) diff --git a/mdoc/Mono.Documentation/Updater/Formatters/ILFullMemberFormatter.cs b/mdoc/Mono.Documentation/Updater/Formatters/ILFullMemberFormatter.cs index ad30e238..b54e64d9 100644 --- a/mdoc/Mono.Documentation/Updater/Formatters/ILFullMemberFormatter.cs +++ b/mdoc/Mono.Documentation/Updater/Formatters/ILFullMemberFormatter.cs @@ -440,7 +440,7 @@ namespace Mono.Documentation.Updater.Formatters return buf.Append (end); } - private StringBuilder AppendParameter (StringBuilder buf, ParameterDefinition parameter) + protected override StringBuilder AppendParameter (StringBuilder buf, ParameterDefinition parameter) { if (parameter.ParameterType is ByReferenceType) { @@ -599,5 +599,10 @@ namespace Mono.Documentation.Updater.Formatters return buf.ToString (); } + + protected override void AppendFunctionPointerTypeName(StringBuilder buf, FunctionPointerType type, IAttributeParserContext context) + { + buf.Append("method"); + } } }
\ No newline at end of file diff --git a/mdoc/Mono.Documentation/Updater/Formatters/JsFormatter.cs b/mdoc/Mono.Documentation/Updater/Formatters/JsFormatter.cs index dabceb3e..b7f4d25c 100644 --- a/mdoc/Mono.Documentation/Updater/Formatters/JsFormatter.cs +++ b/mdoc/Mono.Documentation/Updater/Formatters/JsFormatter.cs @@ -136,6 +136,11 @@ namespace Mono.Documentation.Updater.Formatters return buf.Append(string.Join(", ", parameters.Select(i => i.Name))); } + protected override StringBuilder AppendParameter(StringBuilder buf, ParameterDefinition parameter) + { + return buf.Append(parameter.Name); + } + protected MethodDefinition GetConstructor(TypeDefinition type) { return type.GetConstructors() diff --git a/mdoc/Mono.Documentation/Updater/Formatters/MemberFormatter.cs b/mdoc/Mono.Documentation/Updater/Formatters/MemberFormatter.cs index 6e1ca5bd..9e057319 100644 --- a/mdoc/Mono.Documentation/Updater/Formatters/MemberFormatter.cs +++ b/mdoc/Mono.Documentation/Updater/Formatters/MemberFormatter.cs @@ -1,4 +1,5 @@ using Mono.Cecil; +using Mono.Documentation.Updater.Formatters; using Mono.Documentation.Util; using System; using System.Collections.Generic; @@ -131,6 +132,69 @@ namespace Mono.Documentation.Updater return AppendArrayModifiers (buf, (ArrayType)type); } + protected virtual void AppendFunctionPointerTypeName(StringBuilder buf, FunctionPointerType type, IAttributeParserContext context) + { + buf.Append("delegate*"); + + var callingConvention = GetCallingConvention(type); + if (callingConvention != MethodCallingConvention.Default.ToString()) + { + buf.Append(" unmanaged"); + if (!string.IsNullOrEmpty(callingConvention)) + { + buf.Append("[").Append(callingConvention).Append("]"); + } + } + + buf.Append("<"); + if (type.Parameters?.Count > 0) + { + for (int i = 0; i < type.Parameters.Count; i++) + { + AppendParameter(buf, type.Parameters[i]); + buf.Append(", "); + } + } + AppendReturnTypeName(buf, type, true); + buf.Append(">"); + } + + private string GetCallingConvention(FunctionPointerType type) + { + var callingConvention = type.CallingConvention.ToString("D"); + // Cecil lib uses "9" to stands for "Unmanaged Ext" + if (callingConvention != "9") + { + return NormalizeCallingConvention(type.CallingConvention); + } + else + { + StringBuilder buf = new StringBuilder(); + AssembleCallingConvention(type.ReturnType, buf); + return buf.ToString(); + } + } + + private string NormalizeCallingConvention(MethodCallingConvention callingConvention) + { + if (callingConvention == MethodCallingConvention.C) return "Cdecl"; + var callConv = callingConvention.ToString().ToLower(); + return char.ToUpper(callConv[0]) + callConv.Substring(1); + } + + private void AssembleCallingConvention(TypeReference type, StringBuilder buf) + { + if (!(type is OptionalModifierType optionalModifierType)) return; + + var modifier = optionalModifierType.ModifierType.FullName; + if (modifier.StartsWith(Consts.CallConvPrefix)) + { + if (!string.IsNullOrEmpty(buf.ToString())) buf.Append(", "); + buf.Append(modifier.Substring(Consts.CallConvPrefix.Length)); + AssembleCallingConvention(optionalModifierType.ElementType, buf); + } + } + protected virtual bool ShouldStripModFromTypeName { get => true; @@ -168,6 +232,11 @@ namespace Mono.Documentation.Updater AppendPointerTypeName (interimBuilder, type, context); return SetBuffer(buf, interimBuilder, useTypeProjection: useTypeProjection); } + if (type is FunctionPointerType functionPointerType) + { + AppendFunctionPointerTypeName(interimBuilder, functionPointerType, context); + return SetBuffer(buf, interimBuilder, useTypeProjection: useTypeProjection); + } if (type is GenericParameter) { AppendTypeName (interimBuilder, type, context); @@ -562,15 +631,14 @@ namespace Mono.Documentation.Updater } - private StringBuilder AppendReturnTypeName (StringBuilder buf, MethodDefinition method) + protected StringBuilder AppendReturnTypeName (StringBuilder buf, IMethodSignature method, bool noTrailingSpace = false) { var context = AttributeParserContext.Create (method.MethodReturnType); var isNullableType = context.IsNullable (); var returnTypeName = GetTypeName (method.ReturnType, context); buf.Append (returnTypeName); buf.Append (GetTypeNullableSymbol (method.ReturnType, isNullableType)); - buf.Append (" "); - + buf.Append (noTrailingSpace ? "" : " "); return buf; } @@ -604,6 +672,11 @@ namespace Mono.Documentation.Updater return buf; } + protected virtual StringBuilder AppendParameter(StringBuilder buf, ParameterDefinition parameterDef) + { + return buf; + } + protected virtual StringBuilder AppendGenericMethodConstraints (StringBuilder buf, MethodDefinition method) { return buf; diff --git a/mdoc/Mono.Documentation/Updater/Formatters/VBFullMemberFormatter.cs b/mdoc/Mono.Documentation/Updater/Formatters/VBFullMemberFormatter.cs index 6d9761f8..dbe5890b 100644 --- a/mdoc/Mono.Documentation/Updater/Formatters/VBFullMemberFormatter.cs +++ b/mdoc/Mono.Documentation/Updater/Formatters/VBFullMemberFormatter.cs @@ -562,7 +562,7 @@ namespace Mono.Documentation.Updater return buf.Append(end); } - private StringBuilder AppendParameter(StringBuilder buf, ParameterDefinition parameter) + protected override StringBuilder AppendParameter(StringBuilder buf, ParameterDefinition parameter) { if (parameter.IsOptional) { diff --git a/mdoc/mdoc.Test/FormatterTests.cs b/mdoc/mdoc.Test/FormatterTests.cs index b39d10c4..0c442f02 100644 --- a/mdoc/mdoc.Test/FormatterTests.cs +++ b/mdoc/mdoc.Test/FormatterTests.cs @@ -483,6 +483,35 @@ namespace mdoc.Test Assert.AreEqual(expectedSignature, methodSignature); } + [TestCase("UnsafeCombine", "public static R UnsafeCombine<T1,T2,R> (delegate*<T1, T2, R> combinator, T1 left, T2 right);")] + [TestCase("UnsafeCombine1", "public static R UnsafeCombine1<T1,T2,R> (delegate* unmanaged[Cdecl]<T1, T2, R> combinator, T1 left, T2 right);")] + [TestCase("UnsafeCombine2", "public static R UnsafeCombine2<T1,T2,T3,R> (delegate* unmanaged[Stdcall]<ref T1, in T2, out T3, R> combinator, T1 left, T2 right, T3 outVar);")] + [TestCase("UnsafeCombine3", "public static R UnsafeCombine3<T1,T2,R> (delegate* unmanaged[Fastcall]<T1, T2, ref R> combinator, T1 left, T2 right);")] + [TestCase("UnsafeCombine4", "public static R UnsafeCombine4<T1,T2,R> (delegate* unmanaged[Thiscall]<T1, T2, ref readonly R> combinator, T1 left, T2 right);")] + [TestCase("UnsafeCombine5", "public static void UnsafeCombine5 (delegate* unmanaged[Cdecl]<void> combinator);")] + [TestCase("UnsafeCombine6", "public static void UnsafeCombine6 (delegate*<delegate* unmanaged[Fastcall]<string, int>, delegate*<string, int>> combinator);")] + [TestCase("UnsafeCombine7", "public static delegate*<delegate* unmanaged[Thiscall]<string, int>, delegate*<string, int>> UnsafeCombine7 ();")] + public void CSharpFuctionPointersTest(string methodName, string expectedSignature) + { + var method = GetMethod(typeof(SampleClasses.FunctionPointers), m => m.Name == methodName); + var methodSignature = formatter.GetDeclaration(method); + Assert.AreEqual(expectedSignature, methodSignature); + } + + [TestCase("UnsafeCombine1", "public static R UnsafeCombine1<T1,T2,R> (delegate* unmanaged<T1, T2, R> combinator, T1 left, T2 right);")] + [TestCase("UnsafeCombine2", "public static R UnsafeCombine2<T1,T2,R> (delegate* unmanaged[Cdecl, SuppressGCTransition]<T1, T2, R> combinator, T1 left, T2 right);")] + [TestCase("UnsafeCombine3", "public static R UnsafeCombine3<T1,T2,R> (delegate* unmanaged[Stdcall, MemberFunction]<T1, T2, R> combinator, T1 left, T2 right);")] + [TestCase("UnsafeCombine4", "public static void UnsafeCombine4 (delegate*<delegate* unmanaged[Cdecl, Fastcall]<string, int>, delegate*<string, int>> combinator);")] + [TestCase("UnsafeCombine5", "public static delegate* unmanaged[Cdecl, Fastcall]<delegate* unmanaged[Thiscall, MemberFunction]<string, int>, delegate*<string, int>> UnsafeCombine5 ();")] + public void CSharpFuctionPointersUnmanagedExtTest(string methodName, string expectedSignature) + { + var functionPointersDllPath = "../../../../external/Test/FunctionPointersTest.dll"; + var type = GetType(functionPointersDllPath, "FunctionPointersTest.FunctionPointers"); + var method = GetMethod(type, m => m.Name == methodName); + var methodSignature = formatter.GetDeclaration(method); + Assert.AreEqual(expectedSignature, methodSignature); + } + #region Helper Methods string RealTypeName(string name){ switch (name) { diff --git a/mdoc/mdoc.Test/MDocUpdaterTests.cs b/mdoc/mdoc.Test/MDocUpdaterTests.cs index 6924df61..4f52a7d8 100644 --- a/mdoc/mdoc.Test/MDocUpdaterTests.cs +++ b/mdoc/mdoc.Test/MDocUpdaterTests.cs @@ -48,6 +48,18 @@ namespace mdoc.Test Assert.AreEqual("Mono_DocTest_Generic.GenericBase<U>", parameterType); } + + [TestCase("UnsafeCombine", "delegate*<T1, T2, R>")] + [TestCase("UnsafeCombineOverload", "delegate*<System.IntPtr, System.UIntPtr, R>")] + public void Test_GetDocParameterType_CSharpFunctionPointer(string methodName, string expected) + { + var method = GetMethod(typeof(SampleClasses.FunctionPointers), methodName); + + string parameterType = MDocUpdater.GetDocParameterType(method.Parameters[0].ParameterType); + + Assert.AreEqual(expected, parameterType); + } + [Test] public void Test_GetNamespace_IgnoredNamespaceGeneric() { diff --git a/mdoc/mdoc.Test/SampleClasses/FunctionPointers.cs b/mdoc/mdoc.Test/SampleClasses/FunctionPointers.cs new file mode 100644 index 00000000..9e84c0b8 --- /dev/null +++ b/mdoc/mdoc.Test/SampleClasses/FunctionPointers.cs @@ -0,0 +1,31 @@ +using System; + +namespace mdoc.Test.SampleClasses +{ + public class FunctionPointers + { + public unsafe static R UnsafeCombine<T1, T2, R>(delegate*<T1, T2, R> combinator, T1 left, T2 right) => + combinator(left, right); + + public unsafe static R UnsafeCombineOverload<R>(delegate*<IntPtr, UIntPtr, R> combinator, IntPtr left, UIntPtr right) => +combinator(left, right); + + public unsafe static R UnsafeCombine1<T1, T2, R>(delegate* unmanaged[Cdecl]<T1, T2, R> combinator, T1 left, T2 right) => + combinator(left, right); + + public unsafe static R UnsafeCombine2<T1, T2, T3, R>(delegate* unmanaged[Stdcall]<ref T1, in T2, out T3, R> combinator, T1 left, T2 right, T3 outVar) => + combinator(ref left, right, out outVar); + + public unsafe static R UnsafeCombine3<T1, T2, R>(delegate* unmanaged[Fastcall]<T1, T2, ref R> combinator, T1 left, T2 right) => + combinator(left, right); + + public unsafe static R UnsafeCombine4<T1, T2, R>(delegate* unmanaged[Thiscall]<T1, T2, ref readonly R> combinator, T1 left, T2 right) => +combinator(left, right); + + public unsafe static void UnsafeCombine5(delegate* unmanaged[Cdecl]<void> combinator) => combinator(); + + public unsafe static void UnsafeCombine6(delegate*<delegate* unmanaged[Fastcall]<string, int>, delegate*<string, int>> combinator) => combinator(null); + + public unsafe static delegate*<delegate* unmanaged[Thiscall]<string, int>, delegate*<string, int>> UnsafeCombine7() => throw null; + } +}
\ No newline at end of file diff --git a/mdoc/mdoc.Test/mdoc.Test.csproj b/mdoc/mdoc.Test/mdoc.Test.csproj index ae540c68..422b12fe 100644 --- a/mdoc/mdoc.Test/mdoc.Test.csproj +++ b/mdoc/mdoc.Test/mdoc.Test.csproj @@ -16,6 +16,7 @@ </PropertyGroup>
<PropertyGroup>
<RunPostBuildEvent>Always</RunPostBuildEvent>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Reference Include="mdoc.Test.Cplusplus, Version=1.0.6709.28740, Culture=neutral, processorArchitecture=x86">
|