xamarin-macios/tests/cecil-tests/ConstructorTest.cs

422 строки
15 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using NUnit.Framework;
using Xamarin.Tests;
using Xamarin.Utils;
namespace Cecil.Tests {
public class ConstructorTest {
static bool IsMatch (MethodDefinition ctor, params (string Namespace, string Name) [] parameterTypes)
{
if (!ctor.IsConstructor)
return false;
if (ctor.IsStatic)
return false;
if (!ctor.HasParameters)
return false;
var parameters = ctor.Parameters;
if (parameters.Count != parameterTypes.Length)
return false;
for (var i = 0; i < parameters.Count; i++) {
if (!parameters [i].ParameterType.Is (parameterTypes [i].Namespace, parameterTypes [i].Name))
return false;
}
return true;
}
static MethodDefinition GetConstructor (TypeDefinition type, params (string Namespace, string Name) [] parameterTypes)
{
foreach (var ctor in type.Methods) {
if (IsMatch (ctor, parameterTypes))
return ctor;
}
return null;
}
static string GetLocation (MethodDefinition method)
{
if (method.DebugInformation.HasSequencePoints) {
var seq = method.DebugInformation.SequencePoints [0];
return seq.Document.Url + ":" + seq.StartLine + ": ";
}
return string.Empty;
}
static bool IsFunctionEnd (IList<Instruction> instructions, int index)
{
if (instructions.Count == index + 1 && instructions [index].OpCode == OpCodes.Ret)
return true;
if (instructions.Count == index + 2 && instructions [index].OpCode == OpCodes.Newobj && ((MethodReference) instructions [index].Operand).Resolve ().Parameters.Count == 0 && instructions [index + 1].OpCode == OpCodes.Throw)
return true;
if (instructions.Count == index + 3 && instructions [index].OpCode == OpCodes.Ldstr && instructions [index + 1].OpCode == OpCodes.Newobj && ((MethodReference) instructions [index + 1].Operand).Resolve ().Parameters.Count == 1 && instructions [index + 2].OpCode == OpCodes.Throw)
return true;
return false;
}
static bool VerifyInstructions (MethodDefinition method, IList<Instruction> instructions, out string reason)
{
reason = null;
// base (owns)
// optional additional statements (either statement, not both):
// IsDirectBinding = false;
// MarkDirtyIfDerived ()
if (instructions.Count >= 3 &&
instructions [0].OpCode == OpCodes.Ldarg_0 &&
instructions [1].OpCode == OpCodes.Ldarg_1 &&
instructions [2].OpCode == OpCodes.Call) {
var targetMethod = (instructions [2].Operand as MethodReference).Resolve ();
if (!targetMethod.IsConstructor) {
reason = $"Calls another method which is not a constructor: {targetMethod.FullName}";
return false;
}
var isChainedCtorCall = targetMethod.DeclaringType == method.DeclaringType || targetMethod.DeclaringType == method.DeclaringType.BaseType;
if (!isChainedCtorCall) {
reason = $"Calls unknown (unchained) constructor: {targetMethod.FullName}";
return false;
}
if (IsFunctionEnd (instructions, 3))
return true;
if (instructions [3].OpCode == OpCodes.Ldarg_0 && instructions [4].OpCode == OpCodes.Ldc_I4_0 && instructions [5].OpCode == OpCodes.Call) {
targetMethod = (instructions [5].Operand as MethodReference).Resolve ();
if (targetMethod.Name != "set_IsDirectBinding") {
reason = $"Calls unknown method: {targetMethod.FullName}";
return false;
}
if (IsFunctionEnd (instructions, 6))
return true;
}
if (instructions [3].OpCode == OpCodes.Ldarg_0 && instructions [4].OpCode == OpCodes.Call) {
targetMethod = (instructions [4].Operand as MethodReference).Resolve ();
if (targetMethod.Name != "MarkDirtyIfDerived") {
reason = $"Calls unknown method: {targetMethod.FullName}";
return false;
}
if (IsFunctionEnd (instructions, 5))
return true;
}
}
// base (handle, owns|false)
if (instructions.Count >= 4 &&
instructions [0].OpCode == OpCodes.Ldarg_0 &&
instructions [1].OpCode == OpCodes.Ldarg_1 &&
(instructions [2].OpCode == OpCodes.Ldarg_2 || instructions [2].OpCode == OpCodes.Ldc_I4_0) &&
instructions [3].OpCode == OpCodes.Call) {
var targetMethod = (instructions [3].Operand as MethodReference).Resolve ();
if (!targetMethod.IsConstructor) {
reason = $"Calls another method which is not a constructor (2): {targetMethod.FullName}";
return false;
}
var isChainedCtorCall = targetMethod.DeclaringType.Resolve () == method.DeclaringType.Resolve () || targetMethod.DeclaringType.Resolve () == method.DeclaringType.BaseType.Resolve ();
if (!isChainedCtorCall) {
reason = $"Calls unknown (unchained) constructor (2): {targetMethod.FullName}";
return false;
}
if (IsFunctionEnd (instructions, 4))
return true;
}
// base (handle, owns|false, validate: false|true)
if (instructions.Count >= 5 &&
instructions [0].OpCode == OpCodes.Ldarg_0 &&
instructions [1].OpCode == OpCodes.Ldarg_1 &&
(instructions [2].OpCode == OpCodes.Ldarg_2 || instructions [2].OpCode == OpCodes.Ldc_I4_0) &&
(instructions [3].OpCode == OpCodes.Ldc_I4_0 || instructions [3].OpCode == OpCodes.Ldc_I4_1) &&
instructions [4].OpCode == OpCodes.Call) {
var targetMethod = (instructions [4].Operand as MethodReference).Resolve ();
if (!targetMethod.IsConstructor) {
reason = $"Calls another method which is not a constructor (2): {targetMethod.FullName}";
return false;
}
var isChainedCtorCall = targetMethod.DeclaringType == method.DeclaringType || targetMethod.DeclaringType == method.DeclaringType.BaseType;
if (!isChainedCtorCall) {
reason = $"Calls unknown (unchained) constructor (2): {targetMethod.FullName}";
return false;
}
if (IsFunctionEnd (instructions, 5))
return true;
}
if (reason is null)
reason = $"Sequence of instructions didn't match any known sequence.";
return false;
}
static bool VerifyConstructor (MethodDefinition ctor, out string failureReason)
{
failureReason = null;
// There's nothing wrong with a constructor that doesn't exist
if (ctor is null)
return true;
// Verify that the constructor only does valid stuff
if (!VerifyInstructions (ctor, ctor.Body.Instructions, out failureReason)) {
Console.WriteLine (ctor.FullName);
foreach (var instr in ctor.Body.Instructions)
Console.WriteLine (instr);
return false;
}
return true;
}
public static bool ImplementsINativeObject (TypeDefinition type)
{
if (type is null)
return false;
foreach (var id in type.Interfaces) {
if (id.InterfaceType.Name == "INativeObject") {
return true;
}
}
return ImplementsINativeObject (type.BaseType?.Resolve ());
}
public static bool SubclassesNSObject (TypeDefinition type)
{
if (type is null)
return false;
if (type.Namespace == "Foundation" && type.Name == "NSObject")
return true;
return SubclassesNSObject (type.BaseType?.Resolve ());
}
static bool IsVisible (TypeDefinition type)
{
if (type.IsNested) {
if (!IsVisible (type.DeclaringType))
return false;
return type.IsNestedPublic || type.IsNestedFamily || type.IsNestedFamilyOrAssembly;
} else {
return type.IsPublic;
}
}
static bool IsVisible (MethodDefinition method)
{
if (!IsVisible (method.DeclaringType))
return false;
return method.IsPublic || method.IsFamilyOrAssembly || method.IsFamily;
}
static bool IsPublic (MethodDefinition method)
{
if (!IsVisible (method.DeclaringType))
return false;
return method.IsPublic;
}
[Test]
[TestCase (ApplePlatform.iOS)]
[TestCase (ApplePlatform.TVOS)]
[TestCase (ApplePlatform.MacCatalyst)]
[TestCase (ApplePlatform.MacOSX)]
public void INativeObjectIntPtrConstructorDoesNotOwnHandle (ApplePlatform platform)
{
Configuration.IgnoreIfIgnoredPlatform (platform);
var failures = new List<string> ();
foreach (var dll in Configuration.GetBaseLibraryImplementations (platform)) {
Console.WriteLine (dll);
using (var ad = AssemblyDefinition.ReadAssembly (dll, new ReaderParameters (ReadingMode.Deferred) { ReadSymbols = true })) {
foreach (var type in ad.MainModule.Types) {
// Skip classes we know aren't (properly) reference counted.
switch (type.Name) {
case "Selector": // not really refcounted
case "Class": // not really refcounted
case "Protocol": // not really refcounted
case "AURenderEventEnumerator": // this class shouldn't really be an INativeObject in the first place
case "AudioBuffers": // this class shouldn't really be an INativeObject in the first place
case "AVAudioChannelLayout": // has a private IntPtr constructor which is a void* in native code (i.e. not a mistake).
case "VNVideoProcessorFrameRateCadence": // has a nint (i.e. IntPtr) constructor (framerate) - not a mistake
case "NSMutableOrderedSet`1":
case "NSMutableOrderedSet": // has a nint (i.e. IntPtr) constructor (capacity) - not a mistake
case "NSMutableSet`1":
case "NSMutableSet": // has a nint (i.e. IntPtr) constructor (capacity) - not a mistake
case "NSMutableString": // has a nint (i.e. IntPtr) constructor (capacity) - not a mistake
case "NSNumber": // has a nint (i.e. IntPtr) constructor - not a mistake
case "NSConditionLock": // has a nint (i.e. IntPtr) constructor (condition) - not a mistake
case "NSScrubberProportionalLayout": // has a nint (i.e. IntPtr) constructor (numberOfVisibleItems) - not a mistake
case "NSIndexSet": // has a nuint (i.e. UIntPtr) constructor (index) - not a mistake
continue;
}
// Find classes that implement INativeObject, but doesn't subclass NSObject.
if (!type.IsClass)
continue;
// Does type implement INativeObject?
if (!ImplementsINativeObject (type))
continue;
var isNSObjectSubclass = SubclassesNSObject (type);
// Find the constructors constructors we care about
var intptrCtor = GetConstructor (type, ("System", "IntPtr"));
var intptrBoolCtor = GetConstructor (type, ("System", "IntPtr"), ("System", "Boolean"));
var nativeHandleCtor = GetConstructor (type, ("ObjCRuntime", "NativeHandle"));
var nativeHandleBoolCtor = GetConstructor (type, ("ObjCRuntime", "NativeHandle"), ("System", "Boolean"));
if (intptrCtor is not null) {
if (IsVisible (intptrCtor)) {
var msg = $"{type}: (IntPtr) constructor found. It should not exist.";
Console.WriteLine ($"{GetLocation (intptrCtor)}{msg}");
failures.Add (msg);
} else {
var msg = $"{type}: private (IntPtr) constructor found. It should probably not exist.";
Console.WriteLine ($"{GetLocation (intptrCtor)}{msg}");
}
}
if (intptrBoolCtor is not null) {
var msg = $"{type}: (IntPtr, bool) constructor found. It should not exist.";
Console.WriteLine ($"{GetLocation (intptrBoolCtor)}{msg}");
failures.Add (msg);
}
if (nativeHandleCtor is not null) {
if (IsPublic (nativeHandleCtor)) {
var msg = $"{type}: public (NativeHandle) constructor found. If it exists it should not be public.";
Console.WriteLine ($"{GetLocation (nativeHandleCtor)}{msg}");
failures.Add (msg);
}
}
if (nativeHandleBoolCtor is not null) {
if (IsPublic (nativeHandleBoolCtor)) {
var msg = $"{type}: public (NativeHandle, bool) constructor found. If it exists it should not be public.";
Console.WriteLine ($"{GetLocation (nativeHandleBoolCtor)}{msg}");
failures.Add (msg);
}
}
var skipILVerification = isNSObjectSubclass;
switch (type.Name) {
case "CGPDFObject": // root class
case "SecKeyChain": // root class
case "NSObject": // NSObject is a base class and needs custom constructor logic
case "NSZone": // root class
case "ABAddressBook": // needs a custom ctor implementation
case "CFSocket": // needs a custom ctor implementation
case "NWBrowser": // needs a custom ctor implementation
case "NWListener": // needs a custom ctor implementation
case "CFNotificationCenter": // needs a custom ctor implementation
case "AUGraph": // needs a custom ctor implementation
case "ABMultiValue`1": // has a custom ctor implementation
skipILVerification = true;
break;
}
if (!skipILVerification) {
if (!VerifyConstructor (nativeHandleCtor, out var failureReason)) {
var msg = $"{type}: (NativeHandle) ctor failed IL verification: {failureReason}";
Console.WriteLine ($"{GetLocation (nativeHandleCtor)}{msg}");
failures.Add (msg);
}
if (!VerifyConstructor (nativeHandleBoolCtor, out failureReason)) {
var msg = $"{type}: (NativeHandle, bool) ctor failed IL verification: {failureReason}";
Console.WriteLine ($"{GetLocation (nativeHandleBoolCtor)}{msg}");
failures.Add (msg);
}
}
}
}
}
Assert.That (failures, Is.Empty, "No failures");
}
bool IsConditionallyPreserved (MethodDefinition ctor)
{
if (!ctor.HasCustomAttributes)
return false;
foreach (var ca in ctor.CustomAttributes) {
if (ca.AttributeType.Name != "PreserveAttribute")
continue;
if (!ca.HasFields)
continue;
foreach (var field in ca.Fields) {
if (field.Name != "Conditional")
continue;
return (bool) field.Argument.Value;
}
}
return false;
}
[Test]
[TestCase (ApplePlatform.iOS)]
[TestCase (ApplePlatform.TVOS)]
[TestCase (ApplePlatform.MacCatalyst)]
[TestCase (ApplePlatform.MacOSX)]
public void NativeObjectIntPtrBoolConstructorIsPreserved (ApplePlatform platform)
{
Configuration.IgnoreIfIgnoredPlatform (platform);
var failures = new List<string> ();
foreach (var dll in Configuration.GetBaseLibraryImplementations (platform)) {
using (var ad = AssemblyDefinition.ReadAssembly (dll, new ReaderParameters (ReadingMode.Deferred) { ReadSymbols = true })) {
foreach (var type in ad.MainModule.Types) {
switch (type.Name) {
case "AudioBuffers": // this class shouldn't really be an INativeObject in the first place
continue;
}
// Find classes that implement INativeObject, but doesn't subclass NSObject.
if (!type.IsClass)
continue;
// Skip abstract classes, any subclasses should preserve their ctor (which will make the linker mark this class' ctor as well).
if (type.IsAbstract)
continue;
// Does type implement INativeObject?
if (!ImplementsINativeObject (type))
continue;
if (SubclassesNSObject (type))
continue;
// Find the IntPtr/NativeHandle, Boolean constructor
var intptrBoolCtor = GetConstructor (type, ("System", "IntPtr"), ("System", "Boolean"));
var nativeHandleBoolCtor = GetConstructor (type, ("ObjCRuntime", "NativeHandle"), ("System", "Boolean"));
if (intptrBoolCtor is not null && !IsConditionallyPreserved (intptrBoolCtor)) {
var msg = $"{type}: The (IntPtr, bool) constructor is not conditionally preserved.";
Console.WriteLine ($"{GetLocation (intptrBoolCtor)}{msg}");
failures.Add (msg);
}
if (nativeHandleBoolCtor is not null && !IsConditionallyPreserved (nativeHandleBoolCtor)) {
var msg = $"{type}: The (NativeHandle, bool) constructor is not conditionally preserved.";
Console.WriteLine ($"{GetLocation (nativeHandleBoolCtor)}{msg}");
failures.Add (msg);
}
}
}
}
Assert.That (failures, Is.Empty, "No failures");
}
}
}