From b52f30d4412c849398cf9fba02a1954cf8083056 Mon Sep 17 00:00:00 2001 From: David Maas Date: Sun, 13 Feb 2022 17:44:26 -0600 Subject: [PATCH] Added new infrastructure for generating and manipulating user-friendly trampoline methods. This commit also adds partial support for .NET 7 / C# 11. Closes https://github.com/MochiLibraries/Biohazrd/issues/236 Closes https://github.com/MochiLibraries/Biohazrd/issues/79 Fixes https://github.com/MochiLibraries/Biohazrd/issues/200 Contributes to https://github.com/MochiLibraries/Biohazrd/issues/233 Contributes to https://github.com/MochiLibraries/Biohazrd/issues/237 Workaround for https://github.com/MochiLibraries/Biohazrd/issues/239 --- .../#Declarations/NativeBooleanDeclaration.cs | 12 +- .../#Declarations/NativeCharDeclaration.cs | 12 +- .../CSharpTranslationVerifier.cs | 155 +++- .../CSharpTranslationVerifierPass2.cs | 2 + .../CreateTrampolinesTransformation.cs | 304 ++++++++ ...ttableTypesWhereNecessaryTransformation.cs | 4 + .../#TypeReferences/ByRefTypeReference.cs | 55 ++ Biohazrd.CSharp/BiohzardExtensions.cs | 21 +- Biohazrd.CSharp/ByRefKind.cs | 25 + Biohazrd.CSharp/CSharpGenerationOptions.cs | 13 + .../CSharpLibraryGenerator.Functions.cs | 25 +- ...LibraryGenerator.ICSharpOutputGenerator.cs | 32 +- .../CSharpLibraryGenerator.Records.cs | 35 +- .../CSharpLibraryGenerator.WriteType.cs | 33 +- Biohazrd.CSharp/CSharpLibraryGenerator.cs | 43 ++ Biohazrd.CSharp/CastKind.cs | 8 + .../Infrastructure/ICSharpOutputGenerator.cs | 7 + .../ICSharpOutputGeneratorInternal.cs | 10 + .../Metadata/SetLastErrorFunction.cs | 17 +- Biohazrd.CSharp/NonBlittableTypeKind.cs | 7 + Biohazrd.CSharp/TargetLanguageVersion.cs | 1 + Biohazrd.CSharp/TargetRuntime.cs | 1 + Biohazrd.CSharp/Trampolines/Adapter.cs | 217 ++++++ Biohazrd.CSharp/Trampolines/ByRefAdapter.cs | 47 ++ .../Trampolines/ByRefReturnAdapter.cs | 57 ++ Biohazrd.CSharp/Trampolines/CastAdapter.cs | 50 ++ .../Trampolines/CastReturnAdapter.cs | 84 +++ .../Trampolines/IAdapterWithEpilogue.cs | 6 + .../IAdapterWithGenericParameter.cs | 7 + .../Trampolines/IAdapterWithInnerWrapper.cs | 7 + Biohazrd.CSharp/Trampolines/IReturnAdapter.cs | 13 + .../Trampolines/IShortReturnAdapter.cs | 9 + .../Trampolines/NonBlittableTypeAdapter.cs | 51 ++ .../NonBlittableTypeReturnAdapter.cs | 58 ++ .../Trampolines/PassthroughAdapter.cs | 44 ++ .../Trampolines/PassthroughReturnAdapter.cs | 77 ++ .../ReturnByImplicitBufferAdapter.cs | 77 ++ .../Trampolines/SetLastSystemErrorAdapter.cs | 43 ++ .../Trampolines/SpecialAdapterKind.cs | 8 + .../Trampolines/SyntheticAdapter.cs | 18 + .../Trampolines/ThisPointerAdapter.cs | 35 + .../Trampolines/ToPointerAdapter.cs | 31 + Biohazrd.CSharp/Trampolines/Trampoline.cs | 683 ++++++++++++++++++ .../Trampolines/TrampolineBuilder.cs | 163 +++++ .../Trampolines/TrampolineCollection.cs | 193 +++++ .../Trampolines/TrampolineContext.cs | 37 + .../Trampolines/VoidReturnAdapter.cs | 36 + .../Common/TypeReductionTransformation.cs | 8 +- .../RawTypeTransformationBase.cs | 2 +- .../#Declarations/TranslatedDeclaration.cs | 14 + .../ClangDeclTranslatedTypeReference.cs | 3 + .../DeclarationIdTranslatedTypeReference.cs | 3 + .../#TypeReferences/PointerTypeReference.cs | 3 + .../PreResolvedTypeReference.cs | 3 + .../TranslatedTypeReference.cs | 3 + Biohazrd/Biohazrd.csproj | 2 + Biohazrd/DeclarationId.cs | 2 + Biohazrd/DeclarationMetadata.cs | 10 + Biohazrd/DeclarationReference.cs | 3 + ...SharpTranslationVerifierTrampolineTests.cs | 622 ++++++++++++++++ .../TrampolineCollectionTests.cs | 423 +++++++++++ 61 files changed, 3955 insertions(+), 19 deletions(-) create mode 100644 Biohazrd.CSharp/#Transformations/CreateTrampolinesTransformation.cs create mode 100644 Biohazrd.CSharp/#TypeReferences/ByRefTypeReference.cs create mode 100644 Biohazrd.CSharp/ByRefKind.cs create mode 100644 Biohazrd.CSharp/CastKind.cs create mode 100644 Biohazrd.CSharp/Infrastructure/ICSharpOutputGeneratorInternal.cs create mode 100644 Biohazrd.CSharp/NonBlittableTypeKind.cs create mode 100644 Biohazrd.CSharp/Trampolines/Adapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/ByRefAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/ByRefReturnAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/CastAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/CastReturnAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/IAdapterWithEpilogue.cs create mode 100644 Biohazrd.CSharp/Trampolines/IAdapterWithGenericParameter.cs create mode 100644 Biohazrd.CSharp/Trampolines/IAdapterWithInnerWrapper.cs create mode 100644 Biohazrd.CSharp/Trampolines/IReturnAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/IShortReturnAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/NonBlittableTypeAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/NonBlittableTypeReturnAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/PassthroughAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/PassthroughReturnAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/ReturnByImplicitBufferAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/SetLastSystemErrorAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/SpecialAdapterKind.cs create mode 100644 Biohazrd.CSharp/Trampolines/SyntheticAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/ThisPointerAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/ToPointerAdapter.cs create mode 100644 Biohazrd.CSharp/Trampolines/Trampoline.cs create mode 100644 Biohazrd.CSharp/Trampolines/TrampolineBuilder.cs create mode 100644 Biohazrd.CSharp/Trampolines/TrampolineCollection.cs create mode 100644 Biohazrd.CSharp/Trampolines/TrampolineContext.cs create mode 100644 Biohazrd.CSharp/Trampolines/VoidReturnAdapter.cs create mode 100644 Tests/Biohazrd.CSharp.Tests/CSharpTranslationVerifierTrampolineTests.cs create mode 100644 Tests/Biohazrd.CSharp.Tests/TrampolineCollectionTests.cs diff --git a/Biohazrd.CSharp/#Declarations/NativeBooleanDeclaration.cs b/Biohazrd.CSharp/#Declarations/NativeBooleanDeclaration.cs index 25d017c..2f75a31 100644 --- a/Biohazrd.CSharp/#Declarations/NativeBooleanDeclaration.cs +++ b/Biohazrd.CSharp/#Declarations/NativeBooleanDeclaration.cs @@ -1,6 +1,8 @@ using Biohazrd.CSharp.Infrastructure; using Biohazrd.Transformation; using Biohazrd.Transformation.Infrastructure; +using System; +using System.ComponentModel; using static Biohazrd.CSharp.CSharpCodeWriter; namespace Biohazrd.CSharp @@ -11,6 +13,8 @@ namespace Biohazrd.CSharp /// /// See https://github.com/InfectedLibraries/Biohazrd/issues/99 for details. /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete($"This declaration is only ever added by {nameof(WrapNonBlittableTypesWhereNecessaryTransformation)}, which is deprecated.")] public sealed record NativeBooleanDeclaration : TranslatedDeclaration, ICustomTranslatedDeclaration, ICustomCSharpTranslatedDeclaration { public NativeBooleanDeclaration() @@ -24,11 +28,17 @@ TransformationResult ICustomTranslatedDeclaration.TransformTypeChildren(ITypeTra => this; void ICustomCSharpTranslatedDeclaration.GenerateOutput(ICSharpOutputGenerator outputGenerator, VisitorContext context, CSharpCodeWriter writer) + => Emit(Name, writer); + + internal static void Emit(CSharpCodeWriter writer) + => Emit("NativeBoolean", writer); + + private static void Emit(string name, CSharpCodeWriter writer) { writer.Using("System"); // IComprable, IComparable, IEquatable writer.Using("System.Runtime.InteropServices"); // StructLayoutAttribute, LayoutKind writer.Using("System.Runtime.CompilerServices"); // MethodImplAttribute, MethodImplOptions, Unsafe - string sanitizedName = SanitizeIdentifier(Name); + string sanitizedName = SanitizeIdentifier(name); // Developers should typically not use this type directly anyway, but we provide the same instance methods as System.Boolean // for scenarios where the return type of a native function is immeidately consumed. diff --git a/Biohazrd.CSharp/#Declarations/NativeCharDeclaration.cs b/Biohazrd.CSharp/#Declarations/NativeCharDeclaration.cs index 3fda4c6..d0de98e 100644 --- a/Biohazrd.CSharp/#Declarations/NativeCharDeclaration.cs +++ b/Biohazrd.CSharp/#Declarations/NativeCharDeclaration.cs @@ -1,12 +1,16 @@ using Biohazrd.CSharp.Infrastructure; using Biohazrd.Transformation; using Biohazrd.Transformation.Infrastructure; +using System; +using System.ComponentModel; using static Biohazrd.CSharp.CSharpCodeWriter; namespace Biohazrd.CSharp { /// This type works around the fact that you can't specify marshaling for chars on function pointers. /// See https://github.com/InfectedLibraries/Biohazrd/issues/99 for details. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete($"This declaration is only ever added by {nameof(WrapNonBlittableTypesWhereNecessaryTransformation)}, which is deprecated.")] public sealed record NativeCharDeclaration : TranslatedDeclaration, ICustomTranslatedDeclaration, ICustomCSharpTranslatedDeclaration { public NativeCharDeclaration() @@ -20,11 +24,17 @@ TransformationResult ICustomTranslatedDeclaration.TransformTypeChildren(ITypeTra => this; void ICustomCSharpTranslatedDeclaration.GenerateOutput(ICSharpOutputGenerator outputGenerator, VisitorContext context, CSharpCodeWriter writer) + => Emit(Name, writer); + + internal static void Emit(CSharpCodeWriter writer) + => Emit("NativeChar", writer); + + private static void Emit(string name, CSharpCodeWriter writer) { writer.Using("System"); // IComparable, IComparable, IEquatable writer.Using("System.Runtime.InteropServices"); // StructLayoutAttribute, LayoutKind writer.Using("System.Runtime.CompilerServices"); // MethodImplAttribute, MethodImplOptions, Unsafe - string sanitizedName = SanitizeIdentifier(Name); + string sanitizedName = SanitizeIdentifier(name); // Developers should typically not use this type directly anyway, but we provide the same instance methods as System.Char // for scenarios where the return type of a native function is immeidately consumed. diff --git a/Biohazrd.CSharp/#Transformations/CSharpTranslationVerifier.cs b/Biohazrd.CSharp/#Transformations/CSharpTranslationVerifier.cs index ef4e674..05aca17 100644 --- a/Biohazrd.CSharp/#Transformations/CSharpTranslationVerifier.cs +++ b/Biohazrd.CSharp/#Transformations/CSharpTranslationVerifier.cs @@ -1,9 +1,13 @@ using Biohazrd.CSharp.Metadata; +using Biohazrd.CSharp.Trampolines; using Biohazrd.Expressions; using Biohazrd.Transformation; +using Biohazrd.Transformation.Infrastructure; using ClangSharp; using ClangSharp.Pathogen; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; namespace Biohazrd.CSharp @@ -153,7 +157,145 @@ protected override TransformationResult TransformEnumConstant(TransformationCont protected override TransformationResult TransformFunction(TransformationContext context, TranslatedFunction declaration) { - //TODO: Verify return type is compatible + // Validate the trampolines + if (!declaration.Metadata.TryGet(out TrampolineCollection trampolines)) + { declaration = declaration.WithWarning($"Function does not have trampolines which will soon be deprecated, use {nameof(CreateTrampolinesTransformation)} to add them."); } + else if (trampolines.__OriginalFunction is null) + { + declaration = declaration with + { + Diagnostics = declaration.Diagnostics.Add(Severity.Warning, $"Function has trampolines but they were defaulted. Trampolines should be added via {nameof(CreateTrampolinesTransformation)}."), + Metadata = declaration.Metadata.Remove() + }; + } + else + { + TranslatedFunction original = trampolines.__OriginalFunction; + + // Validate name did not change too late + if (declaration.Name != original.Name) + { declaration = declaration.WithWarning($"Function's name was changed from '{original.Name}' to '{declaration.Name}' after trampolines were added. This change will not be reflected in the output."); } + + // Validate return type did not change too late + if (declaration.ReturnType != original.ReturnType) + { declaration = declaration.WithWarning($"Function's return type was changed from '{original.ReturnType}' to '{declaration.ReturnType}' after trampolines were added. This change will not be reflected in the output."); } + + // Validate the parameter count did not change too late + // (This situation is somewhat nonsensical, but it *is* possible so we need ot handle it somehow.) + if (declaration.Parameters.Length != original.Parameters.Length) + { declaration = declaration.WithWarning($"The number of parameters changed from {original.Parameters.Length} to {declaration.Parameters.Length} after trampolines were added. This change will not be reflected in the output."); } + else + { + // Validate the parameter names and types did not change too late + ArrayTransformHelper verifiedParameters = new(declaration.Parameters); + for (int i = 0; i < declaration.Parameters.Length; i++) + { + TranslatedParameter parameter = declaration.Parameters[i]; + TranslatedParameter originalParameter = original.Parameters[i]; + + if (parameter.Type != originalParameter.Type) + { parameter = parameter.WithWarning($"Parameter's type was changed from '{originalParameter.Type}' to '{parameter.Type}' after trampolines were added. This change will not be reflected in the output."); } + + if (parameter.Name != originalParameter.Name) + { parameter = parameter.WithWarning($"Parameter's name was changed from '{originalParameter.Name}' to '{parameter.Name}' after trampolines were added. This change will not be reflected in the output."); } + + if (parameter.DefaultValue != originalParameter.DefaultValue) + { parameter = parameter.WithWarning($"Parameter's default value was changed from '{originalParameter.DefaultValue}' to '{parameter.DefaultValue}' after trampolines were added. This change will not be reflected in the output."); } + + verifiedParameters.Add(parameter); + } + + if (verifiedParameters.WasChanged) + { + declaration = declaration with + { + Parameters = verifiedParameters.MoveToImmutable() + }; + } + } + + // Validate the trampoline collection does not contain duplicates + for (int i = 0; i < trampolines.SecondaryTrampolines.Length; i++) + { + Trampoline trampoline = trampolines.SecondaryTrampolines[i]; + + if (ReferenceEquals(trampoline, trampolines.PrimaryTrampoline)) + { + trampolines = trampolines with { SecondaryTrampolines = trampolines.SecondaryTrampolines.RemoveAt(i) }; + i--; + declaration = declaration with + { + Diagnostics = declaration.Diagnostics.Add(Severity.Warning, $"Trampoline '{trampoline.Description}' was added as both a secondary and the primary trampoline. The extra entry was removed."), + Metadata = declaration.Metadata.Set(trampolines) + }; + continue; + } + + for (int j = i + 1; j < trampolines.SecondaryTrampolines.Length; j++) + { + if (ReferenceEquals(trampoline, trampolines.SecondaryTrampolines[j])) + { + trampolines = trampolines with { SecondaryTrampolines = trampolines.SecondaryTrampolines.RemoveAt(j) }; + j--; + declaration = declaration with + { + Diagnostics = declaration.Diagnostics.Add(Severity.Warning, $"Trampoline '{trampoline.Description}' was added to the secondary trampoline list more than once. The extra entry was removed."), + Metadata = declaration.Metadata.Set(trampolines) + }; + } + } + } + + // Validate the trampoline graph is complete + // This has to be done iteratively so if we have A -> B -> (missing) and A is checked before B we will remove both. + bool foundBrokenLinks; + do + { + foundBrokenLinks = false; + + // Validate primary trampoline + Trampoline primary = trampolines.PrimaryTrampoline; + if (!primary.IsNativeFunction && !trampolines.Contains(primary.Target)) + { + trampolines = trampolines with { PrimaryTrampoline = trampolines.NativeFunction }; + declaration = declaration with + { + Diagnostics = declaration.Diagnostics.Add(Severity.Warning, $"Trampoline '{primary.Description}' referenced missing trampoline '{primary.Target.Description}' and was removed."), + Metadata = declaration.Metadata.Set(trampolines) + }; + + foundBrokenLinks = true; + } + + // Verify secondary trampolines + foreach (Trampoline trampoline in trampolines.SecondaryTrampolines) + { + if (trampoline.IsNativeFunction) + { + Debug.Fail("Secondary trampolines should not be native trampolines!"); + continue; + } + + if (!trampolines.Contains(trampoline.Target)) + { + trampolines = trampolines with + { + SecondaryTrampolines = trampolines.SecondaryTrampolines.Remove(trampoline, ReferenceEqualityComparer.Instance) + }; + + declaration = declaration with + { + Diagnostics = declaration.Diagnostics.Add(Severity.Warning, $"Trampoline '{trampoline.Description}' referenced missing trampoline '{trampoline.Target.Description}' and was removed."), + Metadata = declaration.Metadata.Set(trampolines) + }; + + foundBrokenLinks = true; + } + } + } while (foundBrokenLinks); + } + + //TODO: Verify the return type can be actually be emitted //TODO: We might want to check if they can be resolved in an extra pass due to BrokenDeclarationExtractor. if (!context.IsValidFieldOrMethodContext()) { declaration = declaration.WithError("Loose functions are not supported in C#."); } @@ -221,10 +363,19 @@ protected override TransformationResult TransformFunction(TransformationContext protected override TransformationResult TransformParameter(TransformationContext context, TranslatedParameter declaration) { - //TODO: Verify type is compatible + //TODO: Verify the type can be actually be emitted if (context.ParentDeclaration is not TranslatedFunction) { declaration = declaration.WithError("Function parameters are not valid outside of a function context."); } + //TODO: This verification was written prior to trampolines. It doesn't really make as much sense anymore now that we have them. + // In particular, it's too late to remove the default values here since they were captured upon trampoline creation. This isn't a problem though since trampolines will skip defaults they don't + // support automatically. + // + // However, it is still nice for generator authors to know when a default parameter value is (probably) being ignored by Biohazrd. As such this logic (and CSharpTranslationVerifierPass2) remain + // for the time being. + // + // In the future we should instead look through all trampolines and determine if a default parameter value was lost entirely for all trampolines and emit warnings for that. + // Verify default parameter value is compatible switch (declaration.DefaultValue) { diff --git a/Biohazrd.CSharp/#Transformations/CSharpTranslationVerifierPass2.cs b/Biohazrd.CSharp/#Transformations/CSharpTranslationVerifierPass2.cs index 3a29cbe..f8934c4 100644 --- a/Biohazrd.CSharp/#Transformations/CSharpTranslationVerifierPass2.cs +++ b/Biohazrd.CSharp/#Transformations/CSharpTranslationVerifierPass2.cs @@ -44,6 +44,8 @@ protected override TransformationResult TransformFunction(TransformationContext if (parameter.DefaultValue is not null) { + //TODO: Technically this isn't necessary during verification anymore as it happens automatically during trampoline emit. + // However you don't get any warnings when it happens during trampoline emit so let's keep it for now. newParameters[i] = parameter with { DefaultValue = null, diff --git a/Biohazrd.CSharp/#Transformations/CreateTrampolinesTransformation.cs b/Biohazrd.CSharp/#Transformations/CreateTrampolinesTransformation.cs new file mode 100644 index 0000000..50ff993 --- /dev/null +++ b/Biohazrd.CSharp/#Transformations/CreateTrampolinesTransformation.cs @@ -0,0 +1,304 @@ +using Biohazrd.CSharp.Metadata; +using Biohazrd.CSharp.Trampolines; +using Biohazrd.Transformation; +using ClangSharp.Pathogen; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace Biohazrd.CSharp; + +public sealed class CreateTrampolinesTransformation : CSharpTransformationBase +{ + public TargetRuntime TargetRuntime { get; init; } = CSharpGenerationOptions.Default.TargetRuntime; //TODO: This ideally should come from some central context to ensure consistency + + /// Enables or disables emitting C++ reference returns as C# reference returns. + /// + /// If enabled, functions such as int& Hello(); will be emitted as ref int Hello();. If disabled (the default), the function will be emitted as int* Hello(); + /// + /// It is recommended you only enable this only if it makes a lot of sense for your particular library. + /// C# developers are not typically used to dealing with ref returns and are likely to mistakenly dereference them unintentionally. + /// Additionally, passing them to other translated methods as pointer parameters is much more cumbersome than the equivalent C++ code. + /// + /// Consider the following C++ API: + /// ```cpp + /// int& Hello(); + /// Increment(int* x); + /// ``` + /// + /// In C++ you might use these functions together like so: + /// ```cpp + /// int& x = Hello(); + /// Increment(&x); + /// ``` + /// + /// With this property enabled, usage from C# looks like this: + /// ```csharp + /// ref int x = ref Hello(); + /// fixed (int* xP = &x) + /// { Increment(xP); } + /// ``` + /// + /// Or if the developer is familiar with : + /// ```csharp + /// ref int x = ref Hello(); + /// Increment((int*)Unsafe.AsPointer(ref x)); + /// ``` + /// + /// Both are quite a bit more clunky than the C++ equivalent. Additionally, C#-style refs cannot be stored in fields (unlike with C++) without odd hacks. + /// + public bool EmitCppReferenceReturnsAsCSharpReferenceReturns { get; init; } = false; + + protected override TransformationResult TransformFunction(TransformationContext context, TranslatedFunction declaration) + { + // Don't try to add trampolines to a function which already has them + if (declaration.Metadata.Has()) + { + Debug.Fail("This should be the only transformation which adds trampoline collections."); + return declaration; + } + + // Can't generate trampolines when the function ABI is unknown + if (declaration.FunctionAbi is null) + { return declaration; } + + // Create diagnostic accumulator for diagnostics relating to the default friendly trampoline + ImmutableArray.Builder? friendlyTrampolineDiagnostics = null; + void PrimaryTrampolineProblem(Severity severity, string message) + { + friendlyTrampolineDiagnostics ??= ImmutableArray.CreateBuilder(); + friendlyTrampolineDiagnostics.Add(severity, message); + } + + // Build dummy native trampoline + IReturnAdapter? nativeReturnAdapter = null; + ImmutableArray.Builder nativeAdapters; + { + int expectedNativeParameterCount = declaration.Parameters.Length; + + // Add parameter slot for this pointer + if (declaration.IsInstanceMethod) + { expectedNativeParameterCount++; } + + // Add parameter slot for return buffer + if (declaration.ReturnByReference) + { expectedNativeParameterCount++; } + + nativeAdapters = ImmutableArray.CreateBuilder(expectedNativeParameterCount); + } + + IReturnAdapter? friendlyReturnAdapter = null; + Dictionary? friendlyAdapters = null; + List? friendlySyntheticAdapters = null; + + void AddFriendlyAdapter(Adapter target, Adapter adapter) + { + friendlyAdapters ??= new Dictionary(nativeAdapters.Count); + friendlyAdapters.Add(target, adapter); + } + + void AddFriendlySyntheticAdapter(SyntheticAdapter adapter) + { + friendlySyntheticAdapters ??= new List(); + friendlySyntheticAdapters.Add(adapter); + } + + // Handle return type when not returning by reference + if (!declaration.ReturnByReference) + { + TypeReference returnType = declaration.ReturnType; + + // Handle returning bool + if (TargetRuntime < TargetRuntime.Net7 && returnType.IsCSharpType(context.Library, CSharpBuiltinType.Bool)) + { + // If the function is virtual, use NativeBoolean on the native side and allow the friendly side to just be passthrough + if (declaration.IsVirtual) + { nativeReturnAdapter = NonBlittableTypeReturnAdapter.NativeBoolean; } + else + { + nativeReturnAdapter = new PassthroughReturnAdapter(CSharpBuiltinType.Byte); + friendlyReturnAdapter = new CastReturnAdapter(nativeReturnAdapter, CSharpBuiltinType.Bool, CastKind.UnsafeAs); + } + } + // Handle virtual methods returning bool + else if (declaration.IsVirtual && TargetRuntime < TargetRuntime.Net7 && returnType.IsCSharpType(context.Library, CSharpBuiltinType.Char)) + { nativeReturnAdapter = NonBlittableTypeReturnAdapter.NativeChar; } + // Handle returning void + else if (returnType is VoidTypeReference) + { nativeReturnAdapter = VoidReturnAdapter.Instance; } + // Handle typical return + else + { + nativeReturnAdapter = new PassthroughReturnAdapter(returnType); + + // Handle C++-style reference + if (EmitCppReferenceReturnsAsCSharpReferenceReturns && returnType is PointerTypeReference { WasReference: true } referenceType) + { friendlyReturnAdapter = new ByRefReturnAdapter(nativeReturnAdapter, referenceType.InnerIsConst ? ByRefKind.RefReadOnly : ByRefKind.Ref); } + } + } + + // Handle implicit parameters + { + void CreateNativeReturnByReferenceAdapter() + { + TypeReference returnType = declaration.ReturnType; + TypeReference returnBufferType = new PointerTypeReference(returnType); + + // Create native return adapter and return buffer parameter + Debug.Assert(nativeReturnAdapter is null); + nativeReturnAdapter = new PassthroughReturnAdapter(returnBufferType); + Adapter returnBufferParameter = new PassthroughAdapter(declaration, SpecialAdapterKind.ReturnBuffer, returnBufferType); + nativeAdapters.Add(returnBufferParameter); + + // Create friendly adapter for return buffer + ReturnByImplicitBufferAdapter returnByReferenceAdapter = new(returnBufferParameter); + friendlyReturnAdapter = returnByReferenceAdapter; + AddFriendlyAdapter(returnBufferParameter, returnByReferenceAdapter); + } + + // Add return buffer before this pointer + // (This also handles normal non-implicit-by-reference return.) + if (declaration.ReturnByReference && !declaration.FunctionAbi.ReturnInfo.Flags.HasFlag(PathogenArgumentFlags.IsSRetAfterThis)) + { CreateNativeReturnByReferenceAdapter(); } + + // Add this pointer + if (declaration.IsInstanceMethod) + { + TypeReference thisType; + if (context.ParentDeclaration is TranslatedRecord parentRecord) + { thisType = new PointerTypeReference(TranslatedTypeReference.Create(parentRecord)); } + else + { + thisType = VoidTypeReference.PointerInstance; + PrimaryTrampolineProblem(Severity.Warning, "Cannot generate primary trampoline: `this` type is unknown."); + } + + Adapter thisPointer = new PassthroughAdapter(declaration, SpecialAdapterKind.ThisPointer, thisType); + nativeAdapters.Add(thisPointer); + AddFriendlyAdapter(thisPointer, new ThisPointerAdapter(thisPointer)); + } + + // Add return buffer after this pointer + if (declaration.ReturnByReference && declaration.FunctionAbi.ReturnInfo.Flags.HasFlag(PathogenArgumentFlags.IsSRetAfterThis)) + { CreateNativeReturnByReferenceAdapter(); } + } + + // We should have a native return adapter by this point + Debug.Assert(nativeReturnAdapter is not null); + + // Handle explicit parameters + foreach (TranslatedParameter parameter in declaration.Parameters) + { + // Handle implicit pass by reference + if (parameter.ImplicitlyPassedByReference) + { + Adapter nativeAdapter = new PassthroughAdapter(parameter, new PointerTypeReference(parameter.Type)); + nativeAdapters.Add(nativeAdapter); + + // Parameters which are written as being passed by value but are implicitly passed by reference will be adapted to behave the same. + // Using byref here might seem tempting from a performance standpoint, but doing so would change semantics since the callee assumes it owns the buffer. + // In theory if the native function receives a const byval we could use a readonly byref, but in partice C++ compilers don't do that even for PODs so we won't either. + // (const byvals are weird and are not consdiered a good practice in C++ anyway.) + AddFriendlyAdapter(nativeAdapter, new ToPointerAdapter(nativeAdapter)); + + continue; + } + + // Handle pre-.NET 7 non-blittables + if (TargetRuntime < TargetRuntime.Net7) + { + // Handle bool + if (parameter.Type.IsCSharpType(context.Library, CSharpBuiltinType.Bool)) + { + // If the function is virtual, use NativeBoolean on the native side and allow the friendly side to just be passthrough + if (declaration.IsVirtual) + { nativeAdapters.Add(new NonBlittableTypeAdapter(parameter, NonBlittableTypeKind.NativeBoolean)); } + else + { + Adapter nativeAdapter = new PassthroughAdapter(parameter, CSharpBuiltinType.Byte); + nativeAdapters.Add(nativeAdapter); + AddFriendlyAdapter(nativeAdapter, new CastAdapter(nativeAdapter, CSharpBuiltinType.Bool, CastKind.UnsafeAs)); + } + continue; + } + + // Handle char -- No friendly adapter needed, it can just be passthrough + if (declaration.IsVirtual && parameter.Type.IsCSharpType(context.Library, CSharpBuiltinType.Char)) + { + nativeAdapters.Add(new NonBlittableTypeAdapter(parameter, NonBlittableTypeKind.NativeChar)); + continue; + } + } + + // Typical case + { + Adapter nativeAdapter = new PassthroughAdapter(parameter); + nativeAdapters.Add(nativeAdapter); + + // Handle C++-style reference + if (parameter.Type is PointerTypeReference { WasReference: true } referenceType) + { AddFriendlyAdapter(nativeAdapter, new ByRefAdapter(nativeAdapter, referenceType.InnerIsConst ? ByRefKind.In : ByRefKind.Ref)); } + } + } + + // Determine if SetLastError logic is needed + bool useLegacySetLastError = false; + if (declaration.Metadata.TryGet(out SetLastErrorFunction setLastErrorMetadata)) + { + // Prior to .NET 6 we have to use the legacy SetLastError logic + if (TargetRuntime < TargetRuntime.Net6) + { + useLegacySetLastError = true; + + if (setLastErrorMetadata.SkipDefensiveClear) + { PrimaryTrampolineProblem(Severity.Warning, $"{nameof(setLastErrorMetadata.SkipDefensiveClear)} is not available when targeting {TargetRuntime}."); } + } + else + { AddFriendlySyntheticAdapter(new SetLastSystemErrorAdapter(setLastErrorMetadata.SkipDefensiveClear)); } + } + + // Create native trampoline + bool haveFriendlyTrampoline = friendlyReturnAdapter is not null || friendlyAdapters is not null || friendlySyntheticAdapters is not null; + Trampoline nativeTrampoline = new(declaration, nativeReturnAdapter, nativeAdapters.ToImmutable()) + { + Name = haveFriendlyTrampoline ? $"{declaration.Name}_PInvoke" : declaration.Name, + Accessibility = haveFriendlyTrampoline ? AccessModifier.Private : declaration.Accessibility, + UseLegacySetLastError = useLegacySetLastError, + }; + Trampoline primaryTrampoline; + + if (!haveFriendlyTrampoline) + { primaryTrampoline = nativeTrampoline; } + else + { + TrampolineBuilder friendlyBuilder = new(nativeTrampoline, useAsTemplate: false) + { + Name = declaration.Name, + Description = "Friendly Overload", + Accessibility = declaration.Accessibility + }; + + if (friendlyReturnAdapter is not null) + { friendlyBuilder.AdaptReturnValue(friendlyReturnAdapter); } + + if (friendlyAdapters is not null) + { friendlyBuilder.AdaptParametersDirect(friendlyAdapters); } + + if (friendlySyntheticAdapters is not null) + { friendlyBuilder.AddSyntheticAdaptersDirect(friendlySyntheticAdapters); } + + primaryTrampoline = friendlyBuilder.Create(); + } + + // Add metadata and diagnostics to the function + ImmutableArray diagnositcs = declaration.Diagnostics; + if (friendlyTrampolineDiagnostics is not null) + { diagnositcs.AddRange(friendlyTrampolineDiagnostics); } + + return declaration with + { + Metadata = declaration.Metadata.Add(new TrampolineCollection(declaration, nativeTrampoline, primaryTrampoline)), + Diagnostics = diagnositcs + }; + } +} diff --git a/Biohazrd.CSharp/#Transformations/WrapNonBlittableTypesWhereNecessaryTransformation.cs b/Biohazrd.CSharp/#Transformations/WrapNonBlittableTypesWhereNecessaryTransformation.cs index 318462d..4716211 100644 --- a/Biohazrd.CSharp/#Transformations/WrapNonBlittableTypesWhereNecessaryTransformation.cs +++ b/Biohazrd.CSharp/#Transformations/WrapNonBlittableTypesWhereNecessaryTransformation.cs @@ -1,9 +1,13 @@ using Biohazrd.Transformation; +using System; +using System.ComponentModel; using System.Diagnostics; using System.Linq; namespace Biohazrd.CSharp { + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete($"Functionality provided by this transformation has been superseded by '{nameof(CreateTrampolinesTransformation)}'")] public sealed class WrapNonBlittableTypesWhereNecessaryTransformation : CSharpTypeTransformationBase { private NativeBooleanDeclaration? NativeBoolean = null; diff --git a/Biohazrd.CSharp/#TypeReferences/ByRefTypeReference.cs b/Biohazrd.CSharp/#TypeReferences/ByRefTypeReference.cs new file mode 100644 index 0000000..2a73f59 --- /dev/null +++ b/Biohazrd.CSharp/#TypeReferences/ByRefTypeReference.cs @@ -0,0 +1,55 @@ +using Biohazrd.CSharp.Infrastructure; +using Biohazrd.Transformation; +using Biohazrd.Transformation.Infrastructure; +using System; + +namespace Biohazrd.CSharp; + +public sealed record ByRefTypeReference : TypeReference, ICustomTypeReference, ICustomCSharpTypeReference +{ + private ByRefKind _Kind; + public ByRefKind Kind + { + get => _Kind; + init + { + if (!Enum.IsDefined(value)) + { throw new ArgumentOutOfRangeException(nameof(value)); } + + _Kind = value; + } + } + + public TypeReference Inner { get; init; } + + public string Keyword => Kind.GetKeyword(); + + public ByRefTypeReference(ByRefKind kind, TypeReference inner) + { + if (!Enum.IsDefined(kind)) + { throw new ArgumentOutOfRangeException(nameof(kind)); } + + _Kind = kind; + Inner = inner; + } + + TypeTransformationResult ICustomTypeReference.TransformChildren(ITypeTransformation transformation, TypeTransformationContext context) + { + DiagnosticAccumulator diagnostics = new(); + SingleTypeTransformHelper newInnerType = new(Inner, ref diagnostics); + + // Transform inner type + newInnerType.SetValue(transformation.TransformTypeRecursively(context, Inner)); + + // Create the result + TypeTransformationResult result = newInnerType.WasChanged ? this with { Inner = newInnerType.NewValue } : this; + result.AddDiagnostics(diagnostics.MoveToImmutable()); + return result; + } + + string ICustomCSharpTypeReference.GetTypeAsString(ICSharpOutputGenerator outputGenerator, VisitorContext context, TranslatedDeclaration declaration) + => $"{Keyword} {outputGenerator.GetTypeAsString(context, declaration, Inner)}"; + + public override string ToString() + => $"{Keyword} {Inner}"; +} diff --git a/Biohazrd.CSharp/BiohzardExtensions.cs b/Biohazrd.CSharp/BiohzardExtensions.cs index f59f1d4..24b28d9 100644 --- a/Biohazrd.CSharp/BiohzardExtensions.cs +++ b/Biohazrd.CSharp/BiohzardExtensions.cs @@ -1,4 +1,5 @@ -using Biohazrd.Expressions; +using Biohazrd.CSharp.Trampolines; +using Biohazrd.Expressions; using Biohazrd.Transformation; using System; using System.Collections.Generic; @@ -68,5 +69,23 @@ internal static TDeclaration WithWarning(this TDeclaration declara NullPointerConstant => CSharpBuiltinType.NativeInt, _ => null }; + + public static Trampoline? TryGetPrimaryTrampoline(this TranslatedFunction function) + => function.Metadata.TryGet(out TrampolineCollection trampolines) ? trampolines.PrimaryTrampoline : null; + + public static Trampoline GetPrimaryTrampoline(this TranslatedFunction function) + => function.TryGetPrimaryTrampoline() ?? throw new InvalidOperationException("Tried to get the primary trampoline of a function with no trampoline metadata."); + + public static TranslatedFunction WithSecondaryTrampoline(this TranslatedFunction function, Trampoline secondaryTrampoline) + { + if (!function.Metadata.TryGet(out TrampolineCollection trampolines)) + { throw new InvalidOperationException("Cannot add a secondary trampoline to a function which has no trampolines."); } + + trampolines = trampolines.WithTrampoline(secondaryTrampoline); + return function with + { + Metadata = function.Metadata.Set(trampolines) + }; + } } } diff --git a/Biohazrd.CSharp/ByRefKind.cs b/Biohazrd.CSharp/ByRefKind.cs new file mode 100644 index 0000000..9bcc8b1 --- /dev/null +++ b/Biohazrd.CSharp/ByRefKind.cs @@ -0,0 +1,25 @@ +using System; + +namespace Biohazrd.CSharp; + +public enum ByRefKind +{ + Ref, + // It might seem odd for In and RefReadOnly to be separate, but it greatly simplifies emitting ByRefTypeReference + RefReadOnly, + In, + Out +} + +public static class ByRefKindExtensions +{ + public static string GetKeyword(this ByRefKind kind) + => kind switch + { + ByRefKind.Ref => "ref", + ByRefKind.RefReadOnly => "ref readonly", + ByRefKind.In => "in", + ByRefKind.Out => "out", + _ => throw new ArgumentOutOfRangeException(nameof(kind)) + }; +} diff --git a/Biohazrd.CSharp/CSharpGenerationOptions.cs b/Biohazrd.CSharp/CSharpGenerationOptions.cs index 3055c33..944888c 100644 --- a/Biohazrd.CSharp/CSharpGenerationOptions.cs +++ b/Biohazrd.CSharp/CSharpGenerationOptions.cs @@ -40,6 +40,7 @@ public TargetRuntime TargetRuntime { TargetLanguageVersion.CSharp9 => TargetRuntime.Net5, TargetLanguageVersion.CSharp10 => TargetRuntime.Net6, + TargetLanguageVersion.CSharp11 => TargetRuntime.Net7, _ => TargetRuntime.Net6 }; } @@ -74,6 +75,7 @@ public TargetLanguageVersion TargetLanguageVersion { TargetRuntime.Net5 => TargetLanguageVersion.CSharp9, TargetRuntime.Net6 => TargetLanguageVersion.CSharp10, + TargetRuntime.Net7 => TargetLanguageVersion.CSharp11, _ => TargetLanguageVersion.CSharp10 }; } @@ -90,6 +92,17 @@ public TargetLanguageVersion TargetLanguageVersion } } + /// If true, default parameter values will not be emitted for non-public APIs. + /// + /// This setting is enabled by default to avoid unecessary metadata bloat. + /// If you intend to manually write trampolines for native functions it may be desirable to turn this off. + /// + public bool SuppressDefaultParameterValuesOnNonPublicMethods { get; init; } = true; + + //TODO: We shold automatically prefix this with whatever is configured on OrganizeOutputFilesByNamespaceTransformation + public string InfrastructureTypesNamespace { get; init; } = "Infrastructure"; + public string InfrastructureTypesDirectoryPath { get; init; } = "Infrastructure"; + public CSharpGenerationOptions() #pragma warning disable CS0618 // Type or member is obsolete => DumpOptions = ClangSharpInfoDumper.DefaultOptions; diff --git a/Biohazrd.CSharp/CSharpLibraryGenerator.Functions.cs b/Biohazrd.CSharp/CSharpLibraryGenerator.Functions.cs index 84e7c56..e18c66a 100644 --- a/Biohazrd.CSharp/CSharpLibraryGenerator.Functions.cs +++ b/Biohazrd.CSharp/CSharpLibraryGenerator.Functions.cs @@ -1,4 +1,5 @@ using Biohazrd.CSharp.Metadata; +using Biohazrd.CSharp.Trampolines; using ClangSharp.Pathogen; using System; using System.Diagnostics; @@ -48,6 +49,17 @@ protected override void VisitFunction(VisitorContext context, TranslatedFunction return; } + if (declaration.Metadata.TryGet(out TrampolineCollection trampolines)) + { + foreach (Trampoline trampoline in trampolines) + { + Writer.EnsureSeparation(); + trampoline.Emit(this, context, declaration, Writer); + } + + return; + } + EmitFunctionContext emitContext = new(context, declaration); // Emit the DllImport @@ -318,7 +330,11 @@ private void EmitFunctionPointerForVTable(VisitorContext context, EmitFunctionCo if (declaration.ReturnByReference) { WriteTypeAsReference(context, declaration, declaration.ReturnType); } else - { WriteType(context, declaration, declaration.ReturnType); } + { + string typeString = GetTypeAsString(context, declaration, declaration.ReturnType); + typeString = FixNonBlittableTypeForFunctionPointer(typeString); + Writer.Write(typeString); + } Writer.Write('>'); } @@ -425,7 +441,12 @@ void WriteOutReturnBuffer(EmitFunctionContext emitContext) if (mode == EmitParameterListMode.TrampolineParameters) { WriteTypeForTrampoline(parameterContext, parameter, parameter.Type); } else - { WriteType(parameterContext, parameter, parameter.Type); } + { + string typeString = GetTypeAsString(parameterContext, parameter, parameter.Type); + if (mode == EmitParameterListMode.VTableFunctionPointerParameters) + { typeString = FixNonBlittableTypeForFunctionPointer(typeString); } + Writer.Write(typeString); + } } if (writeNames) diff --git a/Biohazrd.CSharp/CSharpLibraryGenerator.ICSharpOutputGenerator.cs b/Biohazrd.CSharp/CSharpLibraryGenerator.ICSharpOutputGenerator.cs index 064a114..385f34c 100644 --- a/Biohazrd.CSharp/CSharpLibraryGenerator.ICSharpOutputGenerator.cs +++ b/Biohazrd.CSharp/CSharpLibraryGenerator.ICSharpOutputGenerator.cs @@ -1,10 +1,13 @@ using Biohazrd.CSharp.Infrastructure; using Biohazrd.Expressions; +using System; namespace Biohazrd.CSharp { - partial class CSharpLibraryGenerator : ICSharpOutputGenerator + partial class CSharpLibraryGenerator : ICSharpOutputGenerator, ICSharpOutputGeneratorInternal { + CSharpGenerationOptions ICSharpOutputGenerator.Options => Options; + string ICSharpOutputGenerator.GetTypeAsString(VisitorContext context, TranslatedDeclaration declaration, TypeReference type) => GetTypeAsString(context, declaration, type); @@ -16,5 +19,32 @@ void ICSharpOutputGenerator.AddUsing(string @namespace) void ICSharpOutputGenerator.Visit(VisitorContext context, TranslatedDeclaration declaration) => Visit(context, declaration); + + void ICSharpOutputGenerator.AddDiagnostic(TranslationDiagnostic diagnostic) + => Diagnostics.Add(diagnostic); + + void ICSharpOutputGeneratorInternal.Fatal(VisitorContext context, TranslatedDeclaration declaration, string? reason) + => Fatal(context, declaration, reason); + + void ICSharpOutputGeneratorInternal.Fatal(VisitorContext context, TranslatedDeclaration declaration, string? reason, string? extraDescription) + => Fatal(context, declaration, reason, extraDescription); + + // These are used for a dirty hack to work around lack of proper support for late-generated infrastructure types + private bool __NeedsNativeBoolean; + private bool __NeedsNativeChar; + void ICSharpOutputGeneratorInternal.__IndicateInfrastructureTypeDependency(NonBlittableTypeKind kind) + { + switch (kind) + { + case NonBlittableTypeKind.NativeBoolean: + __NeedsNativeBoolean = true; + break; + case NonBlittableTypeKind.NativeChar: + __NeedsNativeChar = true; + break; + default: + throw new NotSupportedException(); + } + } } } diff --git a/Biohazrd.CSharp/CSharpLibraryGenerator.Records.cs b/Biohazrd.CSharp/CSharpLibraryGenerator.Records.cs index 6737963..6fb665d 100644 --- a/Biohazrd.CSharp/CSharpLibraryGenerator.Records.cs +++ b/Biohazrd.CSharp/CSharpLibraryGenerator.Records.cs @@ -1,4 +1,5 @@ -using System.Linq; +using Biohazrd.CSharp.Trampolines; +using System.Linq; using static Biohazrd.CSharp.CSharpCodeWriter; namespace Biohazrd.CSharp @@ -87,10 +88,36 @@ private void EmitVTable(VisitorContext context, TranslatedVTableField field, Tra Writer.Write($"{entry.Accessibility.ToCSharpKeyword()} "); - if (entry.IsFunctionPointer && entry.MethodReference?.TryResolve(context.Library) is TranslatedFunction associatedFunction) + TranslatedFunction? associatedFunction = null; + + // Prefer methods which are a sibling to the vtable + // (This is a workaround for https://github.com/MochiLibraries/Biohazrd/issues/239) + if (entry.MethodReference is DeclarationReference methodReference && context.ParentDeclaration is TranslatedRecord record) + { + foreach (TranslatedDeclaration sibling in record) + { + if (sibling is TranslatedFunction function && methodReference.__HACK__CouldResolveTo(function)) + { + associatedFunction = function; + break; + } + } + } + + // If we didn't find it as a sibling search the entire library + associatedFunction ??= entry.MethodReference?.TryResolve(context.Library) as TranslatedFunction; + + if (entry.IsFunctionPointer && associatedFunction is not null) { - EmitFunctionContext emitContext = new(context, associatedFunction); - EmitFunctionPointerForVTable(context, emitContext, associatedFunction); + if (associatedFunction.Metadata.TryGet(out TrampolineCollection trampolines)) + { + trampolines.NativeFunction.EmitFunctionPointer(this, context, associatedFunction, Writer); + } + else + { + EmitFunctionContext emitContext = new(context, associatedFunction); + EmitFunctionPointerForVTable(context, emitContext, associatedFunction); + } } else { WriteType(context.Add(entry), entry, VoidTypeReference.PointerInstance); } diff --git a/Biohazrd.CSharp/CSharpLibraryGenerator.WriteType.cs b/Biohazrd.CSharp/CSharpLibraryGenerator.WriteType.cs index 4c1e067..a0312d0 100644 --- a/Biohazrd.CSharp/CSharpLibraryGenerator.WriteType.cs +++ b/Biohazrd.CSharp/CSharpLibraryGenerator.WriteType.cs @@ -136,16 +136,21 @@ private string GetTypeAsString(VisitorContext context, TranslatedDeclaration dec returnType += '*'; functionPointerResult += $"{returnType}, "; } + else + { returnType = FixNonBlittableTypeForFunctionPointer(returnType); } int abiIndex = 0; foreach (TypeReference parameterType in functionPointer.ParameterTypes) { - functionPointerResult += GetTypeAsString(context, declaration, parameterType); + string parameterTypeString = GetTypeAsString(context, declaration, parameterType); // Handle parameters implicitly passed by reference if (haveFunctionAbi && functionPointer.FunctionAbi!.Arguments[abiIndex].Kind == PathogenArgumentKind.Indirect) - { functionPointerResult += '*'; } + { parameterTypeString += '*'; } + else + { parameterTypeString = FixNonBlittableTypeForFunctionPointer(parameterTypeString); } + functionPointerResult += parameterTypeString; functionPointerResult += ", "; abiIndex++; } @@ -170,5 +175,29 @@ private string GetTypeAsString(VisitorContext context, TranslatedDeclaration dec return "int"; } } + + /// Handles types which cannot be blittable in the context of function pointers prior to .NET 7 + /// It is never necessary to call this helper when the type is known to be a pointer + private string FixNonBlittableTypeForFunctionPointer(string typeString) + { + if (Options.TargetRuntime < TargetRuntime.Net7) + { + // We don't want to compare the actual type with CSharpBuiltinType.Bool/Char here because that would not properly account for typedefs and such. + if (typeString == "bool") + { + __NeedsNativeBoolean = true; + Writer.Using(Options.InfrastructureTypesNamespace); + return "NativeBoolean"; + } + else if (typeString == "char") + { + __NeedsNativeChar = true; + Writer.Using(Options.InfrastructureTypesNamespace); + return "NativeChar"; + } + } + + return typeString; + } } } diff --git a/Biohazrd.CSharp/CSharpLibraryGenerator.cs b/Biohazrd.CSharp/CSharpLibraryGenerator.cs index 187d461..91d79c2 100644 --- a/Biohazrd.CSharp/CSharpLibraryGenerator.cs +++ b/Biohazrd.CSharp/CSharpLibraryGenerator.cs @@ -7,6 +7,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.IO; +using System.Linq; using static Biohazrd.CSharp.CSharpCodeWriter; namespace Biohazrd.CSharp @@ -41,6 +42,20 @@ public static ImmutableArray Generate(CSharpGenerationOpt { ImmutableArray.Builder diagnosticsBuilder = ImmutableArray.CreateBuilder(); + // Create AssemblyInfo file + if (options.TargetRuntime >= TargetRuntime.Net7) + { + using CSharpCodeWriter assemblyAttributes = session.Open("AssemblyAttributes.cs"); + assemblyAttributes.Using("System.Runtime.CompilerServices"); + assemblyAttributes.WriteLine("[assembly: DisableRuntimeMarshalling]"); + } + else + { + using CSharpCodeWriter assemblyAttributes = session.Open("AssemblyAttributes.cs"); + assemblyAttributes.Using("System.Runtime.InteropServices"); + assemblyAttributes.WriteLine("[module: DefaultCharSet(CharSet.Unicode)]"); + } + // path => generator Dictionary generators = new(); @@ -80,6 +95,34 @@ public static ImmutableArray Generate(CSharpGenerationOpt generator.Visit(rootVisitorContext, declaration); } + // Emit autolate-generated infrastructure types + // (This is a bit of a hack until we have proper support for these sort of thigns.) + { + CSharpCodeWriter OpenInfrastructureTypeFile(string fileName) + { + fileName = Path.Combine(options.InfrastructureTypesDirectoryPath, fileName); + return session.Open(fileName); + } + + if (generators.Values.Any(g => g.__NeedsNativeBoolean)) + { + using (CSharpCodeWriter writer = OpenInfrastructureTypeFile("NativeBoolean.cs")) + using (writer.Namespace(options.InfrastructureTypesNamespace)) + { + NativeBooleanDeclaration.Emit(writer); + } + } + + if (generators.Values.Any(g => g.__NeedsNativeChar)) + { + using (CSharpCodeWriter writer = OpenInfrastructureTypeFile("NativeChar.cs")) + using (writer.Namespace(options.InfrastructureTypesNamespace)) + { + NativeCharDeclaration.Emit(writer); + } + } + } + // Finish all writers and collect diagnostics foreach (CSharpLibraryGenerator generator in generators.Values) { diff --git a/Biohazrd.CSharp/CastKind.cs b/Biohazrd.CSharp/CastKind.cs new file mode 100644 index 0000000..62cbace --- /dev/null +++ b/Biohazrd.CSharp/CastKind.cs @@ -0,0 +1,8 @@ +namespace Biohazrd.CSharp; + +public enum CastKind +{ + Explicit, + Implicit, + UnsafeAs +} diff --git a/Biohazrd.CSharp/Infrastructure/ICSharpOutputGenerator.cs b/Biohazrd.CSharp/Infrastructure/ICSharpOutputGenerator.cs index 19a7b14..42d730c 100644 --- a/Biohazrd.CSharp/Infrastructure/ICSharpOutputGenerator.cs +++ b/Biohazrd.CSharp/Infrastructure/ICSharpOutputGenerator.cs @@ -4,6 +4,8 @@ namespace Biohazrd.CSharp.Infrastructure { public interface ICSharpOutputGenerator { + CSharpGenerationOptions Options { get; } + string GetTypeAsString(VisitorContext context, TranslatedDeclaration declaration, TypeReference type); string GetConstantAsString(VisitorContext context, TranslatedDeclaration declaration, ConstantValue constant, TypeReference targetType); @@ -11,5 +13,10 @@ public interface ICSharpOutputGenerator void AddUsing(string @namespace); void Visit(VisitorContext context, TranslatedDeclaration declaration); + + void AddDiagnostic(TranslationDiagnostic diagnostic); + + void AddDiagnostic(Severity severity, string message) + => AddDiagnostic(new TranslationDiagnostic(severity, message)); } } diff --git a/Biohazrd.CSharp/Infrastructure/ICSharpOutputGeneratorInternal.cs b/Biohazrd.CSharp/Infrastructure/ICSharpOutputGeneratorInternal.cs new file mode 100644 index 0000000..a8356c4 --- /dev/null +++ b/Biohazrd.CSharp/Infrastructure/ICSharpOutputGeneratorInternal.cs @@ -0,0 +1,10 @@ +namespace Biohazrd.CSharp.Infrastructure; + +internal interface ICSharpOutputGeneratorInternal : ICSharpOutputGenerator +{ + void Fatal(VisitorContext context, TranslatedDeclaration declaration, string? reason, string? extraDescription); + void Fatal(VisitorContext context, TranslatedDeclaration declaration, string? reason); + + // This is used for a dirty hack to work around our lack of proper support for late-generate infrastructure types + void __IndicateInfrastructureTypeDependency(NonBlittableTypeKind kind); +} diff --git a/Biohazrd.CSharp/Metadata/SetLastErrorFunction.cs b/Biohazrd.CSharp/Metadata/SetLastErrorFunction.cs index e57ce75..b414492 100644 --- a/Biohazrd.CSharp/Metadata/SetLastErrorFunction.cs +++ b/Biohazrd.CSharp/Metadata/SetLastErrorFunction.cs @@ -3,7 +3,20 @@ namespace Biohazrd.CSharp.Metadata { /// The presence of this metadata on a function indicates will be set for the corresponding P/Invoke. - /// This metadata item has no affect on virtual methods. + /// + /// Unless targeting .NET 6 or later, this metadata item has no affect on virtual methods. + /// + /// This metadata must be applied prior to . + /// public struct SetLastErrorFunction : IDeclarationMetadataItem - { } + { + /// Skips clearing the last system error before the P/Invoke + /// + /// Leaving this as false will clear the last system error before invoking the native function. + /// This matches the behavior of the built-in .NET marshaler but is generally not needed. + /// + /// Only applicable when targeting .NET 6 or later. + /// + public bool SkipDefensiveClear { get; init; } + } } diff --git a/Biohazrd.CSharp/NonBlittableTypeKind.cs b/Biohazrd.CSharp/NonBlittableTypeKind.cs new file mode 100644 index 0000000..4eea272 --- /dev/null +++ b/Biohazrd.CSharp/NonBlittableTypeKind.cs @@ -0,0 +1,7 @@ +namespace Biohazrd.CSharp; + +internal enum NonBlittableTypeKind +{ + NativeBoolean, + NativeChar +} diff --git a/Biohazrd.CSharp/TargetLanguageVersion.cs b/Biohazrd.CSharp/TargetLanguageVersion.cs index b1b62f8..73c1ab1 100644 --- a/Biohazrd.CSharp/TargetLanguageVersion.cs +++ b/Biohazrd.CSharp/TargetLanguageVersion.cs @@ -5,5 +5,6 @@ public enum TargetLanguageVersion Default = 0, CSharp9 = 90, CSharp10 = 100, + CSharp11 = 110, } } diff --git a/Biohazrd.CSharp/TargetRuntime.cs b/Biohazrd.CSharp/TargetRuntime.cs index 5e67b1c..0f60b77 100644 --- a/Biohazrd.CSharp/TargetRuntime.cs +++ b/Biohazrd.CSharp/TargetRuntime.cs @@ -5,5 +5,6 @@ public enum TargetRuntime Default = 0, Net5 = 50, Net6 = 60, + Net7 = 70, } } diff --git a/Biohazrd.CSharp/Trampolines/Adapter.cs b/Biohazrd.CSharp/Trampolines/Adapter.cs new file mode 100644 index 0000000..b75e28a --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/Adapter.cs @@ -0,0 +1,217 @@ +using Biohazrd.CSharp.Infrastructure; +using Biohazrd.Expressions; +using System; + +namespace Biohazrd.CSharp.Trampolines; + +public abstract class Adapter +{ + public TypeReference InputType { get; protected init; } + public string Name { get; protected init; } + public bool AcceptsInput { get; protected init; } + public bool ProvidesOutput => TargetDeclaration != DeclarationId.Null; + + internal DeclarationId TargetDeclaration { get; } + public ConstantValue? DefaultValue { get; protected init; } + + public SpecialAdapterKind SpecialKind { get; internal init; } + + private protected Adapter(TranslatedParameter target) + { + InputType = target.Type; + Name = target.Name; + AcceptsInput = true; + + TargetDeclaration = target.Id; + DefaultValue = target.DefaultValue; + + SpecialKind = SpecialAdapterKind.None; + } + + private protected Adapter(TranslatedFunction target, SpecialAdapterKind specialKind, TypeReference inputType) + { + AcceptsInput = true; + TargetDeclaration = target.Id; + DefaultValue = null; + SpecialKind = specialKind; + + if (specialKind == SpecialAdapterKind.ThisPointer) + { + if (!target.IsInstanceMethod) + { throw new ArgumentException("The specified function is not an instance method and as such cannot have a this pointer parameter.", nameof(target)); } + + if (inputType is not PointerTypeReference) + { throw new ArgumentException($"The this pointer parameter '{inputType}' is not a pointer type.", nameof(inputType)); } + + InputType = inputType; + Name = "this"; + } + else if (specialKind == SpecialAdapterKind.ReturnBuffer) + { + if (!target.ReturnByReference) + { throw new ArgumentException("The specified function does not implicitly return by reference and as such cannot have a return buffer parameter.", nameof(target)); } + + if (inputType is not PointerTypeReference pointerType) + { throw new ArgumentException($"The return buffer parameter '{inputType}' is not a pointer type.", nameof(inputType)); } + else if (pointerType.Inner != target.ReturnType) + { throw new ArgumentException($"The return buffer parameter '{inputType}' is not a pointer to the function's return type '{target.ReturnType}'.", nameof(inputType)); } + + InputType = inputType; + Name = "__returnBuffer"; + } + else if (specialKind == SpecialAdapterKind.None) + { throw new ArgumentException("Adapters targeting functions rather than their parameters must have a special kind.", nameof(specialKind)); } + else + { throw new ArgumentException($"The specified special kind '{specialKind}' is invalid or unsupported.", nameof(specialKind)); } + } + + protected Adapter(Adapter target) + { + if (!target.AcceptsInput) + { throw new ArgumentException("The target adapter does not accept an input!", nameof(target)); } + + InputType = target.InputType; + Name = target.Name; + AcceptsInput = true; + + TargetDeclaration = target.TargetDeclaration; + DefaultValue = target.DefaultValue; + + SpecialKind = target.SpecialKind; + } + + private protected Adapter(TypeReference inputType, string parameterName) + { + InputType = inputType; + Name = parameterName; + AcceptsInput = true; + + TargetDeclaration = DeclarationId.Null; + DefaultValue = null; + + SpecialKind = SpecialAdapterKind.None; + } + + public bool CorrespondsTo(TranslatedParameter parameter) + { + // Early out: Special adapters never correspond to parameters + if (SpecialKind != SpecialAdapterKind.None) + { return false; } + + // If we don't have a target (IE: This is an input-only adapter) we can't correspond to a parameter + if (TargetDeclaration == DeclarationId.Null) + { return false; } + + return parameter.MatchesId(TargetDeclaration); + } + + public virtual void WriteInputType(TrampolineContext context, CSharpCodeWriter writer) + { + if (!AcceptsInput) + { throw new InvalidOperationException("This adapter does not accept input."); } + + context.WriteType(InputType); + } + + //TODO: Use an analyzer to enforce that this is overriden if WriteInputParameter is overridden + public virtual bool CanEmitDefaultValue(TranslatedLibrary library) + { + // Can't emit a default value if we don't accept input + if (!AcceptsInput) + { throw new InvalidOperationException("This adapter does not accept input."); } + + // Can't emit a default value when there isn't one + if (DefaultValue is null) + { return false; } + + // Can't emit a default value when the constant isn't supported by Biohazrd + if (DefaultValue is UnsupportedConstantExpression) + { return false; } + + // Figure out the effective input type + TypeReference inputType = InputType; + { + // If the input type is a byref we only support default values if its in-byref + if (inputType is ByRefTypeReference byRefInputType) + { + if (byRefInputType.Kind != ByRefKind.In) + { return false; } + + inputType = byRefInputType.Inner; + } + + // If the input type is a typedef we redsolve it + while (inputType is TranslatedTypeReference translatedType) + { + if (translatedType.TryResolve(library) is TranslatedTypedef typedef) + { inputType = typedef.UnderlyingType; } + else + { break; } + } + } + + // Check if the constant is compatible with the type + switch (DefaultValue) + { + // Assume custom types are compatible + //TODO: Maybe ICustomCSharpConstantValue should have to implement a method for checking? + case ICustomCSharpConstantValue: + return true; + // Double and float constants are supported with their corresponding C# built-in types + case DoubleConstant: + return inputType == CSharpBuiltinType.Double; + case FloatConstant: + return inputType == CSharpBuiltinType.Float; + // Integer constants are supported for all C# built-ins as well as enums + //TODO: How should we handle overflow/underflow here? Right now it's not handled at all and will result in invalid codegen. + // We could reject it here, but I think a better solution would be a verification warning and an unchecked cast on emit. + // (This should never happen unless a generator author forces it to happen with a transformation, so let's wait and see what the real-world scenario is.) + case IntegerConstant: + { + if (inputType is CSharpBuiltinTypeReference) + { return true; } + else if (inputType == CSharpBuiltinType.NativeInt || inputType == CSharpBuiltinType.NativeUnsignedInt) + { return true; } + else if (inputType is TranslatedTypeReference translatedType && translatedType.TryResolve(library) is TranslatedEnum) + { return true; } + else + { return false; } + } + // Nulls can be emitted for pointer types + case NullPointerConstant: + return inputType is PointerTypeReference or FunctionPointerTypeReference; + // Strings are not supported by default + case StringConstant: + return inputType == CSharpBuiltinType.String; + default: + return false; + } + } + + public virtual void WriteInputParameter(TrampolineContext context, CSharpCodeWriter writer, bool emitDefaultValue) + { + WriteInputType(context, writer); + writer.Write(' '); + writer.WriteIdentifier(Name); + + if (emitDefaultValue) + { + //TODO: Should this be an assert? The default implementation of CanEmitDefaultValue is a bit heavy, doesn't cache, and a well-formed application + // will not call us with emitDefaultValue = true when it returns false. Alternatively we could just add caching to CanEmitDefaultValue. + if (!CanEmitDefaultValue(context.Context.Library)) + { throw new InvalidOperationException("Caller requested we emit the default value but it's not supported by this adapter."); } + + // It is possible for implementations to override `CanEmitDefaultValue` to return `true` even when `DefaultValue`. + // This is OK (it's assumed the implementation has special default value logic) but it's not OK for it to not override us to do the emit. + if (DefaultValue is null) + { throw new NotSupportedException($"Default implementation of {nameof(WriteInputParameter)} cannot emit a default value without {nameof(DefaultValue)} being set."); } + + writer.Write(" = "); + context.WriteConstant(DefaultValue, InputType); + } + } + + public abstract void WritePrologue(TrampolineContext context, CSharpCodeWriter writer); + public abstract bool WriteBlockBeforeCall(TrampolineContext context, CSharpCodeWriter writer); + public abstract void WriteOutputArgument(TrampolineContext context, CSharpCodeWriter writer); +} diff --git a/Biohazrd.CSharp/Trampolines/ByRefAdapter.cs b/Biohazrd.CSharp/Trampolines/ByRefAdapter.cs new file mode 100644 index 0000000..e0cc5a2 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/ByRefAdapter.cs @@ -0,0 +1,47 @@ +using System; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class ByRefAdapter : Adapter +{ + private TypeReference OutputType { get; } + private string TemporaryName { get; } + + public ByRefKind Kind { get; } + + public ByRefAdapter(Adapter target, ByRefKind kind) + : base(target) + { + if (target.InputType is not PointerTypeReference pointerType) + { throw new ArgumentException("By ref adapters must target pointers!", nameof(target)); } + + if (!Enum.IsDefined(kind)) + { throw new ArgumentOutOfRangeException(nameof(kind)); } + else if (kind == ByRefKind.RefReadOnly) + { throw new ArgumentException("ref readonly is not valid in this context.", nameof(kind)); } + + OutputType = pointerType; + InputType = new ByRefTypeReference(kind, pointerType.Inner); + Name = target.Name; + TemporaryName = $"__{Name}P"; + Kind = kind; + } + + public override void WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { } + + public override bool WriteBlockBeforeCall(TrampolineContext context, CSharpCodeWriter writer) + { + writer.Write("fixed ("); + context.WriteType(OutputType); + writer.Write(' '); + writer.WriteIdentifier(TemporaryName); + writer.Write(" = &"); + writer.WriteIdentifier(Name); + writer.WriteLine(')'); + return true; + } + + public override void WriteOutputArgument(TrampolineContext context, CSharpCodeWriter writer) + => writer.WriteIdentifier(TemporaryName); +} diff --git a/Biohazrd.CSharp/Trampolines/ByRefReturnAdapter.cs b/Biohazrd.CSharp/Trampolines/ByRefReturnAdapter.cs new file mode 100644 index 0000000..d7a5d21 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/ByRefReturnAdapter.cs @@ -0,0 +1,57 @@ +using System; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class ByRefReturnAdapter : IReturnAdapter, IShortReturnAdapter +{ + private readonly TypeReference TargetOutputType; + public TypeReference OutputType { get; } + public string TemporaryName => "__result"; + public ByRefKind Kind { get; } + + public ByRefReturnAdapter(IReturnAdapter target, ByRefKind kind) + { + if (kind != ByRefKind.Ref && kind != ByRefKind.RefReadOnly) + { + if (Enum.IsDefined(kind)) + { throw new ArgumentException($"{kind.GetKeyword()} is not valid in this context.", nameof(kind)); } + else + { throw new ArgumentOutOfRangeException(nameof(kind)); } + } + + if (target.OutputType is not PointerTypeReference pointerType) + { throw new ArgumentException("The target of this adapter must return a pointer.", nameof(target.OutputType)); } + + Kind = kind; + OutputType = new ByRefTypeReference(Kind, pointerType.Inner); + TargetOutputType = pointerType; + } + + void IShortReturnAdapter.WriteShortPrologue(TrampolineContext context, CSharpCodeWriter writer) + { } + + void IShortReturnAdapter.WriteShortReturn(TrampolineContext context, CSharpCodeWriter writer) + => writer.Write("return ref *"); + + void IReturnAdapter.WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { + // Ref locals must be assigned upon declaration in C#, so we keep things as a pointer and conver to a reference at the very end. + context.WriteType(TargetOutputType); + writer.Write(' '); + writer.WriteIdentifier(TemporaryName); + writer.WriteLine(';'); + } + + void IReturnAdapter.WriteResultCapture(TrampolineContext context, CSharpCodeWriter writer) + { + writer.WriteIdentifier(TemporaryName); + writer.Write(" = "); + } + + void IReturnAdapter.WriteEpilogue(TrampolineContext context, CSharpCodeWriter writer) + { + writer.Write("return ref *"); + writer.WriteIdentifier(TemporaryName); + writer.WriteLine(';'); + } +} diff --git a/Biohazrd.CSharp/Trampolines/CastAdapter.cs b/Biohazrd.CSharp/Trampolines/CastAdapter.cs new file mode 100644 index 0000000..c5c8c05 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/CastAdapter.cs @@ -0,0 +1,50 @@ +using System; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class CastAdapter : Adapter +{ + public TypeReference TargetType { get; } + public CastKind Kind { get; } + + public CastAdapter(Adapter target, TypeReference inputType, CastKind kind) + : base(target) + { + if (!Enum.IsDefined(kind)) + { throw new ArgumentOutOfRangeException(nameof(kind)); } + + TargetType = target.InputType; + InputType = inputType; + Kind = kind; + } + + public override void WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { } + + public override bool WriteBlockBeforeCall(TrampolineContext context, CSharpCodeWriter writer) + => false; + + public override void WriteOutputArgument(TrampolineContext context, CSharpCodeWriter writer) + { + switch (Kind) + { + case CastKind.Explicit: + writer.Write('('); + context.WriteType(TargetType); + writer.Write(')'); + writer.WriteIdentifier(Name); + break; + case CastKind.Implicit: + writer.WriteIdentifier(Name); + break; + case CastKind.UnsafeAs: + writer.Using("System.Runtime.CompilerServices"); // Unsafe + writer.Write("Unsafe.As(ref "); + writer.WriteIdentifier(Name); + writer.Write(')'); + break; + default: + throw new InvalidOperationException("Cast kind is invalid!"); + } + } +} diff --git a/Biohazrd.CSharp/Trampolines/CastReturnAdapter.cs b/Biohazrd.CSharp/Trampolines/CastReturnAdapter.cs new file mode 100644 index 0000000..f7a9fc2 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/CastReturnAdapter.cs @@ -0,0 +1,84 @@ +using System; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class CastReturnAdapter : IReturnAdapter, IShortReturnAdapter +{ + public TypeReference OutputType { get; } + public TypeReference SourceType { get; } + public CastKind Kind { get; } + public string TemporaryName => "__result"; + + public CastReturnAdapter(IReturnAdapter target, TypeReference outputType, CastKind kind) + { + if (!Enum.IsDefined(kind)) + { throw new ArgumentOutOfRangeException(nameof(kind)); } + + OutputType = outputType; + SourceType = target.OutputType; + Kind = kind; + } + + bool IShortReturnAdapter.CanEmitShortReturn => Kind is CastKind.Implicit or CastKind.Explicit; + + void IShortReturnAdapter.WriteShortPrologue(TrampolineContext context, CSharpCodeWriter writer) + { } + + void IShortReturnAdapter.WriteShortReturn(TrampolineContext context, CSharpCodeWriter writer) + { + switch (Kind) + { + case CastKind.Implicit: + writer.Write("return "); + break; + case CastKind.Explicit: + writer.Write("return ("); + context.WriteType(OutputType); + writer.Write(')'); + break; + case CastKind.UnsafeAs: + throw new InvalidOperationException("UnsafeAs casts cannot be short returns!"); + default: + throw new InvalidOperationException("Cast kind is invalid!"); + } + } + + public void WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { + context.WriteType(SourceType); + writer.Write(' '); + writer.WriteIdentifier(TemporaryName); + writer.WriteLine(';'); + } + + public void WriteResultCapture(TrampolineContext context, CSharpCodeWriter writer) + { + writer.WriteIdentifier(TemporaryName); + writer.Write(" = "); + } + + public void WriteEpilogue(TrampolineContext context, CSharpCodeWriter writer) + { + switch (Kind) + { + case CastKind.Implicit: + writer.Write("return "); + writer.WriteIdentifier(TemporaryName); + break; + case CastKind.Explicit: + writer.Write("return ("); + context.WriteType(OutputType); + writer.Write(')'); + writer.WriteIdentifier(TemporaryName); + break; + case CastKind.UnsafeAs: + writer.Using("System.Runtime.CompilerServices"); // Unsafe + writer.Write("return Unsafe.As(ref "); + writer.WriteIdentifier(TemporaryName); + writer.WriteLine(");"); + break; + default: + throw new InvalidOperationException("Cast kind is invalid!"); + } + } +} diff --git a/Biohazrd.CSharp/Trampolines/IAdapterWithEpilogue.cs b/Biohazrd.CSharp/Trampolines/IAdapterWithEpilogue.cs new file mode 100644 index 0000000..7e0ab21 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/IAdapterWithEpilogue.cs @@ -0,0 +1,6 @@ +namespace Biohazrd.CSharp.Trampolines; + +public interface IAdapterWithEpilogue +{ + void WriteEpilogue(TrampolineContext context, CSharpCodeWriter writer); +} diff --git a/Biohazrd.CSharp/Trampolines/IAdapterWithGenericParameter.cs b/Biohazrd.CSharp/Trampolines/IAdapterWithGenericParameter.cs new file mode 100644 index 0000000..5cdda5d --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/IAdapterWithGenericParameter.cs @@ -0,0 +1,7 @@ +namespace Biohazrd.CSharp.Trampolines; + +public interface IAdapterWithGenericParameter +{ + void WriteGenericParameter(TrampolineContext context, CSharpCodeWriter writer); + void WriteGenericConstraint(TrampolineContext context, CSharpCodeWriter writer); +} diff --git a/Biohazrd.CSharp/Trampolines/IAdapterWithInnerWrapper.cs b/Biohazrd.CSharp/Trampolines/IAdapterWithInnerWrapper.cs new file mode 100644 index 0000000..20b1d8e --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/IAdapterWithInnerWrapper.cs @@ -0,0 +1,7 @@ +namespace Biohazrd.CSharp.Trampolines; + +public interface IAdapterWithInnerWrapper +{ + void WriterInnerPrologue(TrampolineContext context, CSharpCodeWriter writer); + void WriteInnerEpilogue(TrampolineContext context, CSharpCodeWriter writer); +} diff --git a/Biohazrd.CSharp/Trampolines/IReturnAdapter.cs b/Biohazrd.CSharp/Trampolines/IReturnAdapter.cs new file mode 100644 index 0000000..f8585cd --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/IReturnAdapter.cs @@ -0,0 +1,13 @@ +namespace Biohazrd.CSharp.Trampolines; + +public interface IReturnAdapter +{ + TypeReference OutputType { get; } + + void WriteReturnType(TrampolineContext context, CSharpCodeWriter writer) + => context.WriteType(OutputType); + + void WritePrologue(TrampolineContext context, CSharpCodeWriter writer); + void WriteResultCapture(TrampolineContext context, CSharpCodeWriter writer); + void WriteEpilogue(TrampolineContext context, CSharpCodeWriter writer); +} diff --git a/Biohazrd.CSharp/Trampolines/IShortReturnAdapter.cs b/Biohazrd.CSharp/Trampolines/IShortReturnAdapter.cs new file mode 100644 index 0000000..23c5c05 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/IShortReturnAdapter.cs @@ -0,0 +1,9 @@ +namespace Biohazrd.CSharp.Trampolines; + +public interface IShortReturnAdapter : IReturnAdapter +{ + bool CanEmitShortReturn => true; + + void WriteShortPrologue(TrampolineContext context, CSharpCodeWriter writer); + void WriteShortReturn(TrampolineContext context, CSharpCodeWriter writer); +} diff --git a/Biohazrd.CSharp/Trampolines/NonBlittableTypeAdapter.cs b/Biohazrd.CSharp/Trampolines/NonBlittableTypeAdapter.cs new file mode 100644 index 0000000..becdca7 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/NonBlittableTypeAdapter.cs @@ -0,0 +1,51 @@ +using Biohazrd.CSharp.Infrastructure; +using System; +using System.Diagnostics; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class NonBlittableTypeAdapter : Adapter +{ + internal NonBlittableTypeKind Kind { get; } + + internal NonBlittableTypeAdapter(TranslatedParameter target, NonBlittableTypeKind kind) + : base(target) + { + if (!Enum.IsDefined(kind)) + { throw new ArgumentOutOfRangeException(nameof(kind)); } + + Kind = kind; + InputType = kind switch + { + // Fib about the type we take, the actual types provide implicit conversions from the expected ones + NonBlittableTypeKind.NativeBoolean => CSharpBuiltinType.Bool, + NonBlittableTypeKind.NativeChar => CSharpBuiltinType.Char, + _ => throw new ArgumentOutOfRangeException(nameof(kind)) + }; + } + + public override bool CanEmitDefaultValue(TranslatedLibrary library) + => false; + + public override void WriteInputType(TrampolineContext context, CSharpCodeWriter writer) + { + writer.Using(context.Options.InfrastructureTypesNamespace); + (context.OutputGenerator as ICSharpOutputGeneratorInternal)?.__IndicateInfrastructureTypeDependency(Kind); + writer.Write(Kind.ToString()); + } + + public override void WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + => Debug.Fail("This adapter should not be used in a context where it is emitted in a function body."); + + public override bool WriteBlockBeforeCall(TrampolineContext context, CSharpCodeWriter writer) + { + Debug.Fail("This adapter should not be used in a context where it is emitted in a function body."); + return false; + } + + public override void WriteOutputArgument(TrampolineContext context, CSharpCodeWriter writer) + { + Debug.Fail("This adapter should not be used in a context where it is emitted in a function body."); + writer.WriteIdentifier(Name); + } +} diff --git a/Biohazrd.CSharp/Trampolines/NonBlittableTypeReturnAdapter.cs b/Biohazrd.CSharp/Trampolines/NonBlittableTypeReturnAdapter.cs new file mode 100644 index 0000000..ac01f2d --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/NonBlittableTypeReturnAdapter.cs @@ -0,0 +1,58 @@ +using Biohazrd.CSharp.Infrastructure; +using System; +using System.Diagnostics; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class NonBlittableTypeReturnAdapter : IReturnAdapter +{ + internal NonBlittableTypeKind Kind { get; } + public TypeReference OutputType { get; } + private string TemporaryName => "__result"; + + private NonBlittableTypeReturnAdapter(NonBlittableTypeKind kind) + { + Kind = kind; + OutputType = kind switch + { + // Fib about the type we return, the actual types provide implicit conversions to the expected ones + NonBlittableTypeKind.NativeBoolean => CSharpBuiltinType.Bool, + NonBlittableTypeKind.NativeChar => CSharpBuiltinType.Char, + _ => throw new ArgumentOutOfRangeException(nameof(kind)) + }; + } + + internal static readonly NonBlittableTypeReturnAdapter NativeBoolean = new(NonBlittableTypeKind.NativeBoolean); + internal static readonly NonBlittableTypeReturnAdapter NativeChar = new(NonBlittableTypeKind.NativeChar); + + void IReturnAdapter.WriteReturnType(TrampolineContext context, CSharpCodeWriter writer) + { + writer.Using(context.Options.InfrastructureTypesNamespace); + (context.OutputGenerator as ICSharpOutputGeneratorInternal)?.__IndicateInfrastructureTypeDependency(Kind); + writer.Write(Kind.ToString()); + } + + void IReturnAdapter.WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { + Debug.Fail("This adapter should not be used in a context where it is emitted in a function body."); + context.WriteType(OutputType); + writer.Write(' '); + writer.WriteIdentifier(TemporaryName); + writer.WriteLine(';'); + } + + void IReturnAdapter.WriteResultCapture(TrampolineContext context, CSharpCodeWriter writer) + { + Debug.Fail("This adapter should not be used in a context where it is emitted in a function body."); + writer.WriteIdentifier(TemporaryName); + writer.Write(" = "); + } + + void IReturnAdapter.WriteEpilogue(TrampolineContext context, CSharpCodeWriter writer) + { + Debug.Fail("This adapter should not be used in a context where it is emitted in a function body."); + writer.Write("return "); + writer.WriteIdentifier(TemporaryName); + writer.WriteLine(';'); + } +} diff --git a/Biohazrd.CSharp/Trampolines/PassthroughAdapter.cs b/Biohazrd.CSharp/Trampolines/PassthroughAdapter.cs new file mode 100644 index 0000000..bb45aa7 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/PassthroughAdapter.cs @@ -0,0 +1,44 @@ +using System; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class PassthroughAdapter : Adapter +{ + internal PassthroughAdapter(TranslatedParameter target) + : base(target) + { } + + internal PassthroughAdapter(TranslatedParameter target, TypeReference forcedInputType) + : base(target) + => InputType = forcedInputType; + + internal PassthroughAdapter(TranslatedFunction target, SpecialAdapterKind specialKind, TypeReference inputType) + : base(target, specialKind, inputType) + { } + + public PassthroughAdapter(Adapter target) + : base(target) + { + if (target is IAdapterWithGenericParameter) + { throw new NotSupportedException("Adapting to a generic parameter is not supported."); } + } + + public override void WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { } + + public override bool WriteBlockBeforeCall(TrampolineContext context, CSharpCodeWriter writer) + => false; + + public override void WriteOutputArgument(TrampolineContext context, CSharpCodeWriter writer) + { + if (InputType is ByRefTypeReference byRefType) + { + // In theory we don't have to do this for `in` parameters, but C# allows you to overload between byref in and by value. + // Detecting if this was done is more effort than it's wroth to save 3 characters, so we write out an explicit `in` keyword to ensure the correct overload is chosen. + writer.Write(byRefType.Kind.GetKeyword()); + writer.Write(' '); + } + + writer.WriteIdentifier(Name); + } +} diff --git a/Biohazrd.CSharp/Trampolines/PassthroughReturnAdapter.cs b/Biohazrd.CSharp/Trampolines/PassthroughReturnAdapter.cs new file mode 100644 index 0000000..5c659d5 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/PassthroughReturnAdapter.cs @@ -0,0 +1,77 @@ +using System; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class PassthroughReturnAdapter : IReturnAdapter, IShortReturnAdapter +{ + public TypeReference OutputType { get; } + public string TemporaryName => "__result"; + + internal PassthroughReturnAdapter(TypeReference type) + { + if (type is VoidTypeReference) + { throw new ArgumentException($"Cannot passthrough void type, use {nameof(VoidReturnAdapter)}.", nameof(type)); } + + OutputType = type; + } + + public PassthroughReturnAdapter(IReturnAdapter target) + { + if (target.OutputType is VoidTypeReference) + { throw new ArgumentException("The specified target returns void.", nameof(target)); } + + if (target is IAdapterWithGenericParameter) + { throw new NotSupportedException("Adapting to a generic return type is not supported."); } + + OutputType = target.OutputType; + } + + public void WriteShortPrologue(TrampolineContext context, CSharpCodeWriter writer) + { } + + public void WriteShortReturn(TrampolineContext context, CSharpCodeWriter writer) + { + writer.Write("return "); + + if (OutputType is ByRefTypeReference) + { writer.Write("ref "); } + } + + public void WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { + context.WriteType(OutputType); + writer.Write(' '); + writer.WriteIdentifier(TemporaryName); + + // Ref locals cannot be declared without an initializer + if (OutputType is ByRefTypeReference byRefType) + { + writer.Using("System.Runtime.CompilerServices"); // Unsafe + writer.Write("ref Unsafe.NullRef<"); + context.WriteType(byRefType.Inner); + writer.Write(">()"); + } + + writer.WriteLine(';'); + } + + public void WriteResultCapture(TrampolineContext context, CSharpCodeWriter writer) + { + writer.WriteIdentifier(TemporaryName); + writer.Write(" = "); + + if (OutputType is ByRefTypeReference) + { writer.Write("ref "); } + } + + public void WriteEpilogue(TrampolineContext context, CSharpCodeWriter writer) + { + writer.Write("return "); + + if (OutputType is ByRefTypeReference) + { writer.Write("ref "); } + + writer.WriteIdentifier(TemporaryName); + writer.WriteLine(';'); + } +} diff --git a/Biohazrd.CSharp/Trampolines/ReturnByImplicitBufferAdapter.cs b/Biohazrd.CSharp/Trampolines/ReturnByImplicitBufferAdapter.cs new file mode 100644 index 0000000..f0d9d0a --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/ReturnByImplicitBufferAdapter.cs @@ -0,0 +1,77 @@ +//#define SANITY_CHECK_RETURNED_POINTER +using System; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class ReturnByImplicitBufferAdapter : Adapter, IReturnAdapter +{ + public TypeReference OutputType { get; } + public string TemporaryName => "__returnBuffer"; +#if SANITY_CHECK_RETURNED_POINTER + public string SanityCheckName => "__returnBuffer2"; +#endif + + internal ReturnByImplicitBufferAdapter(Adapter returnBufferParameter) + : base(returnBufferParameter) + { + if (returnBufferParameter.SpecialKind != SpecialAdapterKind.ReturnBuffer) + { throw new ArgumentException("The specified return buffer adapter target is not a return buffer!", nameof(returnBufferParameter)); } + + if (returnBufferParameter.InputType is not PointerTypeReference returnBufferPointerType) + { throw new ArgumentException("The specified return buffer parameter is not a pointer.", nameof(returnBufferParameter)); } + + OutputType = returnBufferPointerType.Inner; + + // This adapter eliminates the return buffer parameter + AcceptsInput = false; + } + + public override void WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { } + + void IReturnAdapter.WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { + context.WriteType(OutputType); + writer.Write(' '); + writer.WriteIdentifier(TemporaryName); + writer.WriteLine(';'); +#if SANITY_CHECK_RETURNED_POINTER + context.WriteType(InputType); + writer.Write(' '); + writer.WriteIdentifier(SanityCheckName); + writer.WriteLine(';'); +#endif + } + + public void WriteResultCapture(TrampolineContext context, CSharpCodeWriter writer) + { +#if SANITY_CHECK_RETURNED_POINTER + writer.WriteIdentifier(SanityCheckName); + writer.Write(" = "); +#endif + } + + public void WriteEpilogue(TrampolineContext context, CSharpCodeWriter writer) + { +#if SANITY_CHECK_RETURNED_POINTER + writer.Using("System.Diagnostics"); // Debug + writer.Write("Debug.Assert("); + writer.WriteIdentifier(SanityCheckName); + writer.Write(" == &"); + writer.WriteIdentifier(TemporaryName); + writer.WriteLine(");"); +#endif + writer.Write("return "); + writer.WriteIdentifier(TemporaryName); + writer.WriteLine(';'); + } + + public override bool WriteBlockBeforeCall(TrampolineContext context, CSharpCodeWriter writer) + => false; + + public override void WriteOutputArgument(TrampolineContext context, CSharpCodeWriter writer) + { + writer.Write('&'); + writer.WriteIdentifier(TemporaryName); + } +} diff --git a/Biohazrd.CSharp/Trampolines/SetLastSystemErrorAdapter.cs b/Biohazrd.CSharp/Trampolines/SetLastSystemErrorAdapter.cs new file mode 100644 index 0000000..1df5b3d --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/SetLastSystemErrorAdapter.cs @@ -0,0 +1,43 @@ +using System.Diagnostics; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class SetLastSystemErrorAdapter : SyntheticAdapter, IAdapterWithInnerWrapper +{ + /// Skips clearing the last system error before the P/Invoke + /// + /// Leaving this as false will clear the last system error before invoking the native function. + /// This matches the behavior of the built-in .NET marshaler but is generally not needed. + /// + public bool SkipPedanticClear { get; } + + public SetLastSystemErrorAdapter(bool skipPedanticClear) + : base("__SetLastError") + { + SkipPedanticClear = skipPedanticClear; + AcceptsInput = false; + } + + public override void WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { } + + public override bool WriteBlockBeforeCall(TrampolineContext context, CSharpCodeWriter writer) + => false; + + void IAdapterWithInnerWrapper.WriterInnerPrologue(TrampolineContext context, CSharpCodeWriter writer) + { + Debug.Assert(context.Options.TargetRuntime >= TargetRuntime.Net6, "The APIs used by this adapter require .NET 6 or later."); + + if (!SkipPedanticClear) + { + writer.Using("System.Runtime.InteropServices"); // Marshal + writer.WriteLine("Marshal.SetLastSystemError(0);"); + } + } + + void IAdapterWithInnerWrapper.WriteInnerEpilogue(TrampolineContext context, CSharpCodeWriter writer) + { + writer.Using("System.Runtime.InteropServices"); // Marshal + writer.WriteLine("Marshal.SetLastPInvokeError(Marshal.GetLastSystemError());"); + } +} diff --git a/Biohazrd.CSharp/Trampolines/SpecialAdapterKind.cs b/Biohazrd.CSharp/Trampolines/SpecialAdapterKind.cs new file mode 100644 index 0000000..70b2a07 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/SpecialAdapterKind.cs @@ -0,0 +1,8 @@ +namespace Biohazrd.CSharp.Trampolines; + +public enum SpecialAdapterKind +{ + None, + ReturnBuffer, + ThisPointer +} diff --git a/Biohazrd.CSharp/Trampolines/SyntheticAdapter.cs b/Biohazrd.CSharp/Trampolines/SyntheticAdapter.cs new file mode 100644 index 0000000..9a237f6 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/SyntheticAdapter.cs @@ -0,0 +1,18 @@ +using System; +using System.Diagnostics; + +namespace Biohazrd.CSharp.Trampolines; + +public abstract class SyntheticAdapter : Adapter +{ + protected SyntheticAdapter(TypeReference inputType, string parameterName) + : base(inputType, parameterName) + => Debug.Assert(!ProvidesOutput); + + protected SyntheticAdapter(string parameterName) + : this(VoidTypeReference.Instance, parameterName) + { } + + public override sealed void WriteOutputArgument(TrampolineContext context, CSharpCodeWriter writer) + => throw new InvalidOperationException("Synthetic adapters do not provide an output argument."); +} diff --git a/Biohazrd.CSharp/Trampolines/ThisPointerAdapter.cs b/Biohazrd.CSharp/Trampolines/ThisPointerAdapter.cs new file mode 100644 index 0000000..cd356ca --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/ThisPointerAdapter.cs @@ -0,0 +1,35 @@ +using System; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class ThisPointerAdapter : Adapter +{ + public ThisPointerAdapter(Adapter target) + : base(target) + { + if (target.SpecialKind != SpecialAdapterKind.ThisPointer) + { throw new ArgumentException("The target adapter is not for the this pointer!", nameof(target)); } + + if (target.InputType is not PointerTypeReference) + { throw new ArgumentException("The target adapter is not a pointer!", nameof(target)); } + + // This adapter eliminates the explicit this pointer parameter + AcceptsInput = false; + } + + public override void WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { } + + public override bool WriteBlockBeforeCall(TrampolineContext context, CSharpCodeWriter writer) + { + writer.Write("fixed ("); + context.WriteType(InputType); + writer.Write(' '); + writer.WriteIdentifier(Name); + writer.WriteLine(" = &this)"); + return true; + } + + public override void WriteOutputArgument(TrampolineContext context, CSharpCodeWriter writer) + => writer.WriteIdentifier(Name); +} diff --git a/Biohazrd.CSharp/Trampolines/ToPointerAdapter.cs b/Biohazrd.CSharp/Trampolines/ToPointerAdapter.cs new file mode 100644 index 0000000..97621bd --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/ToPointerAdapter.cs @@ -0,0 +1,31 @@ +using System; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class ToPointerAdapter : Adapter +{ + public ToPointerAdapter(Adapter target) + : base(target) + { + if (!target.AcceptsInput) + { throw new ArgumentException("The target adapter does not accept an input!", nameof(target)); } + + if (target.InputType is not PointerTypeReference pointerType) + { throw new ArgumentException("By ref adapters must target pointers!", nameof(target)); } + + InputType = pointerType.Inner; + Name = target.Name; + } + + public override void WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { } + + public override bool WriteBlockBeforeCall(TrampolineContext context, CSharpCodeWriter writer) + => false; + + public override void WriteOutputArgument(TrampolineContext context, CSharpCodeWriter writer) + { + writer.Write('&'); + writer.WriteIdentifier(Name); + } +} diff --git a/Biohazrd.CSharp/Trampolines/Trampoline.cs b/Biohazrd.CSharp/Trampolines/Trampoline.cs new file mode 100644 index 0000000..4758917 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/Trampoline.cs @@ -0,0 +1,683 @@ +using Biohazrd.CSharp.Infrastructure; +using Biohazrd.CSharp.Metadata; +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static Biohazrd.CSharp.CSharpCodeWriter; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed record Trampoline +{ + internal Trampoline? Target { get; } + internal DeclarationId TargetFunctionId { get; } + public AccessModifier Accessibility { get; init; } + public string Name { get; init; } + public string Description { get; init; } + public IReturnAdapter ReturnAdapter { get; init; } + public ImmutableArray Adapters { get; init; } + + [MemberNotNullWhen(false, nameof(Target))] + public bool IsNativeFunction => Target is null; + + /// Enables on the emitted P/Invoke. + /// + /// This is a property of the trampoline in order to ensure consistent emit between .NET 5 and more modern runtimes. + /// It should not be used on .NET 6 or newer or exposed publicly. + /// SetLastError behavior should be controlled by applying prior to . + /// + internal bool UseLegacySetLastError { get; init; } + + internal Trampoline(TranslatedFunction function, IReturnAdapter nativeReturnAdapter, ImmutableArray nativeAdapters) + { + Target = null; + TargetFunctionId = function.Id; + Accessibility = function.Accessibility; + Name = function.Name; + Description = function.IsVirtual ? "Native Virtual Method Pointer" : "Native P/Invoke"; + ReturnAdapter = nativeReturnAdapter; + Adapters = nativeAdapters; + Debug.Assert(IsNativeFunction); + Debug.Assert(ReturnAdapter is not IAdapterWithGenericParameter && !Adapters.OfType().Any(), "Native functions cannot have generic parameters."); + } + + internal Trampoline(TrampolineBuilder builder) + { + Trampoline? template = builder.TargetIsTemplate ? builder.Target : null; + Target = template?.Target ?? builder.Target; + Debug.Assert(Target is not null); + TargetFunctionId = builder.Target.TargetFunctionId; + Accessibility = builder.Accessibility; + Name = builder.Name; + Description = builder.Description; + + // Add return adapter + if (builder.ReturnAdapter is not null) + { ReturnAdapter = builder.ReturnAdapter; } + else if (template?.ReturnAdapter is not null) + { ReturnAdapter = template.ReturnAdapter; } + else if (Target.ReturnAdapter.OutputType is VoidTypeReference) + { ReturnAdapter = VoidReturnAdapter.Instance; } + else + { ReturnAdapter = new PassthroughReturnAdapter(Target.ReturnAdapter); } + + // Add parameter adapters + int expectedLength; + if (template is not null) + { expectedLength = template.Adapters.Length; } + else + { + expectedLength = 0; + foreach (Adapter adapter in Target.Adapters) + { + if (adapter.AcceptsInput) + { expectedLength++; } + } + } + + if (builder.SyntheticAdapters is not null) + { expectedLength += builder.SyntheticAdapters.Count; } + + ImmutableArray.Builder adapters = ImmutableArray.CreateBuilder(expectedLength); + + foreach (Adapter targetAdapter in template?.Adapters ?? Target.Adapters) + { + // If the builder adapted this adapter, insert its adapter + if (builder.Adapters?.TryGetValue(targetAdapter, out Adapter? adapter) ?? false) + { adapters.Add(adapter); } + else if (builder.TargetIsTemplate) + { adapters.Add(targetAdapter); } + else if (targetAdapter.AcceptsInput) + { adapters.Add(new PassthroughAdapter(targetAdapter)); } + } + + if (builder.SyntheticAdapters is not null) + { adapters.AddRange(builder.SyntheticAdapters); } + + Adapters = adapters.MoveToImmutable(); + } + + internal void Emit(ICSharpOutputGenerator outputGenerator, VisitorContext context, TranslatedFunction declaration, CSharpCodeWriter writer) + { + if (!declaration.MatchesId(TargetFunctionId)) + { throw new ArgumentException("The specified function is not related to the target of this trampoline.", nameof(declaration)); } + + // Don't emit the native function for virtual methods + if (declaration.IsVirtual && IsNativeFunction) + { return; } + + // If the function is virtual determine how to access it + string? virtualMethodAccess = null; + string? virtualMethodAccessFailure = null; + + if (declaration.IsVirtual && Target is not null && Target.IsNativeFunction) + { + // Figure out how to access the VTable entry + if (context.ParentDeclaration is not TranslatedRecord record) + { virtualMethodAccessFailure = $"Virtual method has no associated class."; } + else if (record.VTableField is null) + { virtualMethodAccessFailure = "Class has no vTable pointer."; } + else if (record.VTable is null) + { virtualMethodAccessFailure = "Class has no virtual method table."; } + else if (declaration.Declaration is null) + { virtualMethodAccessFailure = "Virtual method has no associated Clang declaration."; } + else + { + TranslatedVTableEntry? vTableEntry = null; + + foreach (TranslatedVTableEntry entry in record.VTable.Entries) + { + if (entry.Info.MethodDeclaration == declaration.Declaration.Handle) + { + vTableEntry = entry; + break; + } + } + + if (vTableEntry is null) + { virtualMethodAccessFailure = "Could not find entry in virtual method table."; } + else + { virtualMethodAccess = $"{SanitizeIdentifier(record.VTableField.Name)}->{SanitizeIdentifier(vTableEntry.Name)}"; } + } + + if (virtualMethodAccessFailure is not null) + { outputGenerator.AddDiagnostic(Severity.Error, $"Method trampoline cannot be emitted: {virtualMethodAccessFailure}"); } + + Debug.Assert(virtualMethodAccess is not null || virtualMethodAccessFailure is not null, "We need either a virtual method access or a failure message."); + } + + // Create base function context + TrampolineContext functionContext = new(outputGenerator, this, writer, context, declaration); + + // Scan adapters to figure out how this function will be emitted and to build adapter contexts + bool hasExplicitThis = false; + bool hasAnyEpilogue = false; + bool hasInnerWrapper = false; + bool hasGenericParameters = ReturnAdapter is IAdapterWithGenericParameter; + bool hasDoubleDutyReturnAdapter = false; + int firstDefaultableInput = int.MaxValue; + TrampolineContext[] adapterContexts = new TrampolineContext[Adapters.Length]; + { + int adapterIndex = -1; + VisitorContext parameterVisitorContext = functionContext.Context.Add(declaration); + foreach (Adapter adapter in Adapters) + { + adapterIndex++; + + if (adapter.AcceptsInput) + { + if (adapter.SpecialKind == SpecialAdapterKind.ThisPointer) + { hasExplicitThis = true; } + + // Note: Do not check `adapter.DefaultValue` here. + // Specializations are allowed to implement their own default value emit strategy without leveraging `ConstantValue`. + if (adapter.CanEmitDefaultValue(context.Library)) + { + if (firstDefaultableInput == int.MaxValue) + { firstDefaultableInput = adapterIndex; } + } + else + { firstDefaultableInput = int.MaxValue; } + } + + if (adapter is IAdapterWithEpilogue) + { hasAnyEpilogue = true; } + + if (adapter is IAdapterWithInnerWrapper) + { hasInnerWrapper = hasAnyEpilogue = true; } + + if (adapter is IAdapterWithGenericParameter) + { hasGenericParameters = true; } + + if (ReferenceEquals(adapter, ReturnAdapter)) + { hasDoubleDutyReturnAdapter = true; } + + // Determine the context for this adapter + adapterContexts[adapterIndex] = DetermineAdapterContext(declaration, parameterVisitorContext, adapter, functionContext); + } + } + + //=========================================================================================================================================== + // Emit attributes + //=========================================================================================================================================== + // Hide from Intellisense if applicable + // (Only bother doing this if the method is externally visible + if (Accessibility > AccessModifier.Private) + { + if (declaration.Metadata.Has()) + { + writer.Using("System.ComponentModel"); + writer.WriteLine("[EditorBrowsable(EditorBrowsableState.Never)]"); + } + } + + // Emit DllImport attribute + if (IsNativeFunction) + { + writer.Using("System.Runtime.InteropServices"); + writer.Write($"[DllImport(\"{SanitizeStringLiteral(declaration.DllFileName)}\", CallingConvention = CallingConvention.{declaration.CallingConvention}"); + + if (declaration.MangledName != Name) + { writer.Write($", EntryPoint = \"{SanitizeStringLiteral(declaration.MangledName)}\""); } + + if (UseLegacySetLastError) + { + //Debug.Assert(outputGenerator.Options.TargetRuntime < TargetRuntime.Net6); + writer.Write(", SetLastError = true"); + } + + writer.WriteLine(", ExactSpelling = true)]"); + } + else + { + // Hide from the debugger if applicable + if (outputGenerator.Options.HideTrampolinesFromDebugger) + { + writer.Using("System.Diagnostics"); + writer.WriteLine("[DebuggerStepThrough, DebuggerHidden]"); + } + + EmitMethodImplAttribute(declaration, writer); + + // Obsolete virtual method if it's broken + if (virtualMethodAccessFailure is not null) + { + writer.Using("System"); // ObsoleteAttribute + writer.WriteLine($"[Obsolete(\"Method is broken: {SanitizeStringLiteral(virtualMethodAccessFailure)}\", error: true)]"); + } + } + + //=========================================================================================================================================== + // Emit function signature + //=========================================================================================================================================== + { + if (GetConstructorName(outputGenerator, context, declaration) is string constructorName) + { + writer.Write(Accessibility.ToCSharpKeyword()); + writer.Write(' '); + writer.WriteIdentifier(constructorName); + } + else + { + writer.Write(Accessibility.ToCSharpKeyword()); + + // Write out the static keyword if this is a static method or this is an instance method that still has an explicit this pointer + if (!declaration.IsInstanceMethod || hasExplicitThis) + { writer.Write(" static"); } + + // Write out extern if this is the P/Invoke + if (IsNativeFunction) + { writer.Write(" extern"); } + + // Write out the return type + writer.Write(' '); + ReturnAdapter.WriteReturnType(functionContext, writer); + writer.Write(' '); + + // Write out the function name + writer.WriteIdentifier(Name); + } + + // Write out generic parameters + if (hasGenericParameters) + { + Debug.Assert(!IsNativeFunction); + writer.Write('<'); + bool first = true; + + if (!hasDoubleDutyReturnAdapter && ReturnAdapter is IAdapterWithGenericParameter genericReturnAdapter) + { + genericReturnAdapter.WriteGenericParameter(functionContext, writer); + first = false; + } + + int adapterIndex = -1; + foreach (Adapter adapter in Adapters) + { + adapterIndex++; + + if (adapter is not IAdapterWithGenericParameter genericAdapter) + { continue; } + + if (first) + { first = false; } + else + { writer.Write(", "); } + + genericAdapter.WriteGenericParameter(adapterContexts[adapterIndex], writer); + } + writer.Write('>'); + } + + // Write out the parameter list + { + writer.Write('('); + + bool first = true; + int adapterIndex = -1; + bool skipDefaultValues = outputGenerator.Options.SuppressDefaultParameterValuesOnNonPublicMethods && Accessibility != AccessModifier.Public; + foreach (Adapter adapter in Adapters) + { + adapterIndex++; + + // Nothing to do if the adapter doesn't accept input + if (!adapter.AcceptsInput) + { continue; } + + // Write out the adapter's parameter + if (first) + { first = false; } + else + { writer.Write(", "); } + + bool emitDefaultValue = !skipDefaultValues && adapterIndex >= firstDefaultableInput; + adapter.WriteInputParameter(adapterContexts[adapterIndex], writer, emitDefaultValue); + } + + if (IsNativeFunction) + { + writer.WriteLine(");"); + return; + } + else + { writer.WriteLine(')'); } + } + + // Write out generic constraints + if (hasGenericParameters) + { + using (writer.Indent()) + { + if (!hasDoubleDutyReturnAdapter && ReturnAdapter is IAdapterWithGenericParameter genericReturnAdapter) + { genericReturnAdapter.WriteGenericConstraint(functionContext, writer); } + + int adapterIndex = -1; + foreach (Adapter adapter in Adapters) + { + adapterIndex++; + + if (adapter is not IAdapterWithGenericParameter genericAdapter) + { continue; } + + genericAdapter.WriteGenericConstraint(adapterContexts[adapterIndex], writer); + } + } + } + } + + //=========================================================================================================================================== + // Emit the function body + //=========================================================================================================================================== + + // If this is a broken virtual method trampoline the body should just throw + if (virtualMethodAccessFailure is not null) + { + Debug.Assert(declaration.IsVirtual); + writer.Using("System"); // PlatformNotSupportedException + writer.WriteLineIndented($"=> throw new PlatformNotSupportedException(\"Method is broken: {SanitizeStringLiteral(virtualMethodAccessFailure)}\");"); + return; + } + + using (writer.Block()) + { + IShortReturnAdapter? shortReturn = hasAnyEpilogue ? null : ReturnAdapter as IShortReturnAdapter; + + if (shortReturn is not null && !shortReturn.CanEmitShortReturn) + { shortReturn = null; } + + // Emit out prologues + { + if (shortReturn is not null) + { shortReturn.WriteShortPrologue(functionContext, writer); } + else + { ReturnAdapter.WritePrologue(functionContext, writer); } + + int adapterIndex = -1; + foreach (Adapter adapter in Adapters) + { + adapterIndex++; + adapter.WritePrologue(adapterContexts[adapterIndex], writer); + } + } + + // Emit blocks (IE: `fixed` statements) + writer.EnsureSeparation(); + bool hasBlocks = false; + { + int adapterIndex = -1; + foreach (Adapter adapter in Adapters) + { + adapterIndex++; + + if (adapter.WriteBlockBeforeCall(adapterContexts[adapterIndex], writer)) + { hasBlocks = true; } + } + } + + // Emit function dispatch + void EmitFunctionDispatch() + { + // Handle inner wrapper prologues + if (hasInnerWrapper) + { + int adapterIndex = -1; + foreach (Adapter adapter in Adapters) + { + adapterIndex++; + if (adapter is IAdapterWithInnerWrapper innerWrapper) + { innerWrapper.WriterInnerPrologue(adapterContexts[adapterIndex], writer); } + } + } + + // Emit the actual function dispatch line + if (shortReturn is not null) + { shortReturn.WriteShortReturn(functionContext, writer); } + else + { ReturnAdapter.WriteResultCapture(functionContext, writer); } + + if (virtualMethodAccess is not null) + { + Debug.Assert(declaration.IsVirtual); + writer.Write(virtualMethodAccess); + } + else if (Target.GetConstructorName(outputGenerator, context, declaration) is string constructorName) + { + writer.Write("this = new "); + writer.WriteIdentifier(constructorName); + } + else + { writer.WriteIdentifier(Target.Name); } + + // Emit function arguments + { + writer.Write('('); + + bool first = true; + int adapterIndex = -1; + + foreach (Adapter adapter in Adapters) + { + adapterIndex++; + + if (!adapter.ProvidesOutput) + { continue; } + + if (first) + { first = false; } + else + { writer.Write(", "); } + + adapter.WriteOutputArgument(adapterContexts[adapterIndex], writer); + } + + writer.Write(");"); + } + + // Handle inner wrapper epilogues + if (hasInnerWrapper) + { + writer.WriteLine(); + int adapterIndex = -1; + foreach (Adapter adapter in Adapters) + { + adapterIndex++; + if (adapter is IAdapterWithInnerWrapper innerWrapper) + { innerWrapper.WriteInnerEpilogue(adapterContexts[adapterIndex], writer); } + } + } + } + + if (hasInnerWrapper && hasBlocks) + { + using (writer.Block()) + { EmitFunctionDispatch(); } + } + else if (hasBlocks) + { + // If we need a block but there's no inner wrappers (IE: the dispatch will be on a single line) prefer to write it on a single line to keep things terse. + writer.Write("{ "); + EmitFunctionDispatch(); + writer.WriteLine(" }"); + } + else + { + EmitFunctionDispatch(); + + // If we didn't write inner wrappers, EmitFunctionDispatch will not finish the dispatch with a newline. + if (!hasInnerWrapper) + { writer.WriteLine(); } + } + + // Emit epilogues + if (hasAnyEpilogue || shortReturn is null) + { + writer.EnsureSeparation(); + + if (hasAnyEpilogue) + { + int adapterIndex = -1; + foreach (Adapter adapter in Adapters) + { + adapterIndex++; + + if (adapter is IAdapterWithEpilogue epilogueAdapter) + { epilogueAdapter.WriteEpilogue(adapterContexts[adapterIndex], writer); } + } + } + + ReturnAdapter.WriteEpilogue(functionContext, writer); + } + } + } + + internal void EmitFunctionPointer(ICSharpOutputGeneratorInternal outputGenerator, VisitorContext context, TranslatedFunction declaration, CSharpCodeWriter writer) + { + if (IsNativeFunction) + { + string? callingConventionString = declaration.CallingConvention switch + { + CallingConvention.Cdecl => "Cdecl", + CallingConvention.StdCall => "Stdcall", + CallingConvention.ThisCall => "Thiscall", + CallingConvention.FastCall => "Fastcall", + _ => null + }; + + if (callingConventionString is null) + { + outputGenerator.Fatal(context, declaration, $"The {declaration.CallingConvention} convention is not supported."); + writer.Write("void*"); + return; + } + + writer.Write($"delegate* unmanaged[{callingConventionString}]<"); + } + else + { writer.Write($"delegate*<"); } + + // Create base contexts + TrampolineContext functionContext = new(outputGenerator, this, writer, context, declaration); + VisitorContext parameterVisitorContext = functionContext.Context.Add(declaration); + + foreach (Adapter adapter in Adapters) + { + if (!adapter.AcceptsInput) + { continue; } + + TrampolineContext adapterContext = DetermineAdapterContext(declaration, parameterVisitorContext, adapter, functionContext); + adapter.WriteInputType(adapterContext, writer); + writer.Write(", "); + } + + ReturnAdapter.WriteReturnType(functionContext, writer); + writer.Write('>'); + } + + private TrampolineContext DetermineAdapterContext(TranslatedFunction declaration, in VisitorContext parameterVisitorContext, Adapter adapter, in TrampolineContext functionContext) + { + // This is a lot of extra mostly-unecessary work. + // If this shows up on a profiler we should just remove it change the expectations of the context provided to GetTypeAsString and eliminate it. + // https://github.com/MochiLibraries/Biohazrd/issues/238 + if (adapter.TargetDeclaration == DeclarationId.Null || declaration.MatchesId(adapter.TargetDeclaration)) + { return functionContext; } + else + { + foreach (TranslatedParameter parameter in declaration.Parameters) + { + if (parameter.MatchesId(adapter.TargetDeclaration)) + { + return functionContext with + { + Context = parameterVisitorContext, + Declaration = parameter + }; + } + } + + // This adapter does not match the function or any of its parameters, just use the function's context + Debug.Fail("This should not happen."); + return functionContext; + } + } + + private void EmitMethodImplAttribute(TranslatedFunction declaration, CSharpCodeWriter writer) + { + if (!declaration.Metadata.TryGet(out TrampolineMethodImplOptions optionsMetadata)) + { return; } + + MethodImplOptions options = optionsMetadata.Options; + + if (options == 0) + { return; } + + writer.Using("System.Runtime.CompilerServices"); + writer.Write("[MethodImpl("); + + bool first = true; + foreach (MethodImplOptions option in Enum.GetValues()) + { + if ((options & option) == option) + { + if (first) + { first = false; } + else + { writer.Write(" | "); } + + writer.Write($"{nameof(MethodImplOptions)}.{option}"); + options &= ~option; + } + } + + if (first || options != 0) + { + if (!first) + { writer.Write(" | "); } + + writer.Write($"({nameof(MethodImplOptions)}){(int)options}"); + } + + writer.WriteLine(")]"); + } + + private string? GetConstructorName(ICSharpOutputGenerator outputGenerator, VisitorContext context, TranslatedFunction declaration) + { + // Native functions are never emitted as constructors + if (IsNativeFunction) + { return null; } + + // Non-constructors are never emitted as constructors + if (declaration.SpecialFunctionKind != SpecialFunctionKind.Constructor) + { return null; } + + // If we don't have a parent record we don't know what type we're constructing + if (context.ParentDeclaration is not TranslatedRecord constructorType) + { return null; } + + // If none of our adaptors accept inputs, we won't emit any parameters. Parameterless constructors require C# 10 or newer + if (outputGenerator.Options.TargetLanguageVersion < TargetLanguageVersion.CSharp10) + { + bool haveInputs = false; + foreach (Adapter adapter in Adapters) + { + if (adapter.AcceptsInput) + { + haveInputs = true; + break; + } + } + + if (!haveInputs) + { return null; } + } + + // If we got this far we will emit as a constructor + return constructorType.Name; + } + + public override string ToString() + => $"{Name} ({Description})"; +} diff --git a/Biohazrd.CSharp/Trampolines/TrampolineBuilder.cs b/Biohazrd.CSharp/Trampolines/TrampolineBuilder.cs new file mode 100644 index 0000000..a9a83af --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/TrampolineBuilder.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; + +namespace Biohazrd.CSharp.Trampolines; + +public struct TrampolineBuilder +{ + public Trampoline Target { get; } + internal readonly bool TargetIsTemplate; + public AccessModifier Accessibility { get; set; } + public string Name { get; set; } + public string Description { get; set; } + internal IReturnAdapter? ReturnAdapter { get; private set; } + internal Dictionary? Adapters { get; private set; } + internal List? SyntheticAdapters { get; private set; } + + /// Returns true if this builder has adapters. + /// If this struct is defaulted this will return false. + public bool HasAdapters => ReturnAdapter is not null || Adapters is not null || SyntheticAdapters is not null; + + public TrampolineBuilder(Trampoline target, bool useAsTemplate) + { + // You can't actually use the native function as a template since it's the terminating node of the trampoline graph + // However, for the sake of simplicity we just silently use it as a target instead. + // We don't want generator authors to have to worry about the distinction when adding their own trampolines. + // (IE: This lets generator authors simply target the primary trampoline as a template and get something sensible regardless of whether there's a non-native primary trampoline.) + if (target.IsNativeFunction) + { useAsTemplate = false; } + + Target = target; + TargetIsTemplate = useAsTemplate; + Accessibility = target.Accessibility; + Name = target.Name; + Description = "Unnamed Trampoline"; + ReturnAdapter = null; + Adapters = null; + SyntheticAdapters = null; + } + + public void AdaptReturnValue(IReturnAdapter adapter) + { + if (ReturnAdapter is not null) + { throw new InvalidOperationException("The return adapter has already been specified for this trampoline."); } + + ReturnAdapter = adapter; + } + + public void AdaptParameter(Adapter target, Adapter adapter) + { + if (!Target.Adapters.Contains(target)) + { throw new InvalidOperationException("The specified parameter is not part of the target trampoline."); } + + if (Adapters is null) + { Adapters = new Dictionary(Target.Adapters.Length, ReferenceEqualityComparer.Instance); } + + if (!Adapters.TryAdd(target, adapter)) + { throw new InvalidOperationException("The specified parameter has already been adapted for this trampoline."); } + } + + public bool TryAdaptParameter(TranslatedParameter parameter, Adapter adapter) + { + Adapter? target = null; + foreach (Adapter targetAdapter in Target.Adapters) + { + if (targetAdapter.CorrespondsTo(parameter)) + { + target = targetAdapter; + break; + } + } + + if (target is null) + { return false; } + + AdaptParameter(target, adapter); + return true; + } + + //TODO: This overload is not particularly useful because you need the target adapter to make most adapters + public void AdaptParameter(TranslatedParameter parameter, Adapter adapter) + { + if (!TryAdaptParameter(parameter, adapter)) + { throw new InvalidOperationException($"'{parameter}' is not part of the target trampoline."); } + } + + public bool TryAdaptParameter(SpecialAdapterKind specialParameter, Adapter adapter) + { + if (specialParameter == SpecialAdapterKind.None || !Enum.IsDefined(specialParameter)) + { throw new ArgumentOutOfRangeException(nameof(specialParameter)); } + + Adapter? target = null; + foreach (Adapter targetAdapter in Target.Adapters) + { + if (targetAdapter.SpecialKind == specialParameter) + { + target = targetAdapter; + break; + } + } + + if (target is null) + { return false; } + + AdaptParameter(target, adapter); + return true; + } + + public void AdaptParameter(SpecialAdapterKind specialParameter, Adapter adapter) + { + if (!TryAdaptParameter(specialParameter, adapter)) + { throw new InvalidOperationException($"The target trampoline does not contain a {specialParameter} parameter."); } + } + + //TODO: Add removing and replacing synthetic adapters + // Removing should add a Adapter => null kvp to the adapters list + // Replacing works as normal but should be separtate for API clarity + public void AddSyntheticAdapter(SyntheticAdapter adapter) + { + if (SyntheticAdapters is null) + { SyntheticAdapters = new List(); } + + SyntheticAdapters.Add(adapter); + } + + // This method only exists as an optimization for CreateTrampolinesTransformation so that it can build the friendly trampoline before the native trampoline is complete. + internal void AdaptParametersDirect(Dictionary adapters) + { + if (Adapters is not null) + { throw new InvalidOperationException("This builder has already received adapters."); } + +#if DEBUG + foreach ((Adapter target, Adapter adapter) in adapters) + { AdaptParameter(target, adapter); } +#else + Adapters = adapters; +#endif + } + + internal void AddSyntheticAdaptersDirect(List adapters) + { + if (SyntheticAdapters is not null) + { throw new InvalidOperationException("This builder has already received synthetic adapters."); } + +#if DEBUG + foreach (SyntheticAdapter adapter in adapters) + { AddSyntheticAdapter(adapter); } +#else + SyntheticAdapters = adapters; +#endif + } + + public Trampoline Create() + { + if (Target is null) + { throw new InvalidOperationException("Triend to create a trampoline from a defaulted builder!"); } + + // Changing the name isn't *really* an adaption, but it will create something sane when emitted so let's allow it. + if (!HasAdapters && Name == Target.Name) + { throw new InvalidOperationException("Tried to create a trampoline with nothing adapted!"); } + + return new Trampoline(this); + } +} diff --git a/Biohazrd.CSharp/Trampolines/TrampolineCollection.cs b/Biohazrd.CSharp/Trampolines/TrampolineCollection.cs new file mode 100644 index 0000000..a272a0b --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/TrampolineCollection.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Biohazrd.CSharp.Trampolines; + +public readonly struct TrampolineCollection : IDeclarationMetadataItem, IEnumerable +{ + /// The function declaration used to create this trampoline. Should only be used for verification purposes. + internal TranslatedFunction __OriginalFunction { get; } + + private readonly Trampoline _NativeFunction; + private readonly Trampoline _PrimaryTrampoline; + private ImmutableArray _SecondaryTrampolines { get; init; } + + /// A dummy trampoline representing the low level native function for the associated function. + /// + /// In the case of a function or non-virtual method, this represents the P/Invoke function. + /// + /// In the case of a virtual method this represents the virtual method function pointer. + /// + /// You generally do not want to target this trampoline directly and should either or clone instead. + /// + public Trampoline NativeFunction + { + get => _NativeFunction; + init + { + if (!value.IsNativeFunction) + { throw new ArgumentException("The specified trampoline does not represent a native function.", nameof(value)); } + + if (value.TargetFunctionId != _NativeFunction.TargetFunctionId) + { throw new ArgumentException("The specified trampoline belongs to a different function.", nameof(value)); } + + _NativeFunction = value; + } + } + + /// The primary entry point for the associated function. + /// + /// This represnts the lowest level trampoline without exposing C++ ABI concerns to the callee. (IE: This trampoline will not expose the raw this pointer or implicit return by buffer semantics.) + /// + /// When creating your own trampolines you generally either want to target this trampoline directly or clone+modify it. + /// + /// For functions not involving any special ABI concerns, this may be the same value as . + /// + public Trampoline PrimaryTrampoline + { + get => _PrimaryTrampoline; + init + { + if (value.TargetFunctionId != _NativeFunction.TargetFunctionId) + { throw new ArgumentException("The specified trampoline belongs to a different function.", nameof(value)); } + + // Note: We do not want to ensure the target of this trampoline is present in the collection because we don't necessarily have the full graph available at this point. + // Additionally, this allows transformations which might remove/replace trampolines to not worry about maintaining the trampoline graph. + // (CSharpTranslationVerifier handle validating the trampoline graph is complete and remove any dangling trampolines as necessary.) + + _PrimaryTrampoline = value; + } + } + + /// Secondary trampolines associated with this function, usually added by transformations. + public ImmutableArray SecondaryTrampolines + { + get => _SecondaryTrampolines; + init + { + foreach (Trampoline trampoline in value) + { + if (trampoline.TargetFunctionId != _NativeFunction.TargetFunctionId) + { throw new ArgumentException($"Trampoline '{trampoline}' does not belong to the same function as this collection.", nameof(value)); } + + if (trampoline.IsNativeFunction) + { throw new ArgumentException($"Trampoline '{trampoline}' is a native function. Native functions cannot be trampolines.", nameof(value)); } + + // Note: We do not want to ensure the target of this trampoline is present in the collection because we don't necessarily have the full graph available at this point. + // Additionally, this allows transformations which might remove/replace trampolines to not worry about maintaining the trampoline graph. + // (CSharpTranslationVerifier handle validating the trampoline graph is complete and remove any dangling trampolines as necessary.) + } + + _SecondaryTrampolines = value; + } + } + + internal TrampolineCollection(TranslatedFunction function, Trampoline nativeFunction, Trampoline primaryTrampoline) + { + if (function is null) + { throw new ArgumentNullException(nameof(function)); } + + if (nativeFunction is null) + { throw new ArgumentNullException(nameof(nativeFunction)); } + + if (primaryTrampoline is null) + { throw new ArgumentNullException(nameof(primaryTrampoline)); } + + _NativeFunction = nativeFunction; + _PrimaryTrampoline = primaryTrampoline; + _SecondaryTrampolines = ImmutableArray.Empty; + + // This is only saved for verification purposes + __OriginalFunction = function; + } + + public TrampolineCollection WithTrampoline(Trampoline trampoline) + { + if (trampoline.TargetFunctionId != _NativeFunction.TargetFunctionId) + { throw new ArgumentException("The specified trampoline does not belong to the same function as this collection.", nameof(trampoline)); } + + if (trampoline.IsNativeFunction) + { throw new ArgumentException("Native trampolines cannot be secondary trampolines.", nameof(trampoline)); } + + // Note: We do not want to ensure the target of this trampoline is present in the collection because we don't necessarily have the full graph available at this point. + // Additionally, this allows transformations which might remove/replace trampolines to not worry about maintaining the trampoline graph. + // (CSharpTranslationVerifier handle validating the trampoline graph is complete and remove any dangling trampolines as necessary.) + + return this with + { + _SecondaryTrampolines = _SecondaryTrampolines.Add(trampoline) + }; + } + + internal bool Contains(Trampoline trampoline) + { + if (ReferenceEquals(trampoline, NativeFunction)) + { return true; } + + if (ReferenceEquals(trampoline, PrimaryTrampoline)) + { return true; } + + foreach (Trampoline secondaryTrampoline in SecondaryTrampolines) + { + if (ReferenceEquals(trampoline, secondaryTrampoline)) + { return true; } + } + + return false; + } + + public struct Enumerator + { + private readonly TrampolineCollection Collection; + private int Index; + public Trampoline Current + => Index switch + { + -2 => Collection.NativeFunction, + -1 => Collection.PrimaryTrampoline, + _ => Collection.SecondaryTrampolines[Index] + }; + + internal Enumerator(TrampolineCollection collection) + { + Collection = collection; + Index = -3; + } + + public bool MoveNext() + { + Index++; + + // Skip the primary trampoline if it's the same as the naitve function + if (Index == -1) + { + if (ReferenceEquals(Collection.NativeFunction, Collection.PrimaryTrampoline)) + { Index++; } + } + + return Index < Collection.SecondaryTrampolines.Length; + } + } + + public Enumerator GetEnumerator() + => new Enumerator(this); + + private IEnumerator GetEnumeratorObject() + { + yield return NativeFunction; + + if (!ReferenceEquals(NativeFunction, PrimaryTrampoline)) + { yield return PrimaryTrampoline; } + + foreach (Trampoline trampoline in SecondaryTrampolines) + { yield return trampoline; } + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumeratorObject(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumeratorObject(); +} diff --git a/Biohazrd.CSharp/Trampolines/TrampolineContext.cs b/Biohazrd.CSharp/Trampolines/TrampolineContext.cs new file mode 100644 index 0000000..5d15084 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/TrampolineContext.cs @@ -0,0 +1,37 @@ +using Biohazrd.CSharp.Infrastructure; +using Biohazrd.Expressions; + +namespace Biohazrd.CSharp.Trampolines; + +public struct TrampolineContext +{ + internal ICSharpOutputGenerator OutputGenerator { get; } + public Trampoline Target { get; } + public CSharpCodeWriter Writer { get; } + + internal VisitorContext Context { get; init; } + internal TranslatedDeclaration Declaration { get; set; } + + internal TrampolineContext(ICSharpOutputGenerator outputGenerator, Trampoline target, CSharpCodeWriter writer, VisitorContext context, TranslatedFunction declaration) + { + OutputGenerator = outputGenerator; + Target = target; + Writer = writer; + Context = context; + Declaration = declaration; + } + + public CSharpGenerationOptions Options => OutputGenerator.Options; + + public string GetTypeAsString(TypeReference type) + => OutputGenerator.GetTypeAsString(Context, Declaration, type); + + public void WriteType(TypeReference type) + => Writer.Write(GetTypeAsString(type)); + + public string GetConstantAsString(ConstantValue constant, TypeReference targetType) + => OutputGenerator.GetConstantAsString(Context, Declaration, constant, targetType); + + public void WriteConstant(ConstantValue constant, TypeReference targetType) + => Writer.Write(GetConstantAsString(constant, targetType)); +} diff --git a/Biohazrd.CSharp/Trampolines/VoidReturnAdapter.cs b/Biohazrd.CSharp/Trampolines/VoidReturnAdapter.cs new file mode 100644 index 0000000..65241b5 --- /dev/null +++ b/Biohazrd.CSharp/Trampolines/VoidReturnAdapter.cs @@ -0,0 +1,36 @@ +using System; + +namespace Biohazrd.CSharp.Trampolines; + +public sealed class VoidReturnAdapter : IReturnAdapter, IShortReturnAdapter +{ + public TypeReference OutputType => VoidTypeReference.Instance; + + internal static readonly VoidReturnAdapter Instance = new(); + + private VoidReturnAdapter() + { } + + public static VoidReturnAdapter Create(IReturnAdapter target) + { + if (target.OutputType is not VoidTypeReference) + { throw new ArgumentException("The specified adapter does not return void!", nameof(target)); } + + return Instance; + } + + public void WritePrologue(TrampolineContext context, CSharpCodeWriter writer) + { } + + public void WriteResultCapture(TrampolineContext context, CSharpCodeWriter writer) + { } + + public void WriteEpilogue(TrampolineContext context, CSharpCodeWriter writer) + { } + + public void WriteShortPrologue(TrampolineContext context, CSharpCodeWriter writer) + { } + + public void WriteShortReturn(TrampolineContext context, CSharpCodeWriter writer) + { } +} diff --git a/Biohazrd.Transformation/Common/TypeReductionTransformation.cs b/Biohazrd.Transformation/Common/TypeReductionTransformation.cs index 271f51c..e42ed2a 100644 --- a/Biohazrd.Transformation/Common/TypeReductionTransformation.cs +++ b/Biohazrd.Transformation/Common/TypeReductionTransformation.cs @@ -64,14 +64,18 @@ protected override TypeTransformationResult TransformClangTypeReference(TypeTran case PointerType pointerType: { ClangTypeReference inner = new(pointerType.PointeeType); - return new PointerTypeReference(inner); + return new PointerTypeReference(inner) + { + InnerIsConst = pointerType.PointeeType.IsLocalConstQualified + }; } case ReferenceType referenceType: { ClangTypeReference inner = new(referenceType.PointeeType); TypeTransformationResult result = new PointerTypeReference(inner) { - WasReference = true + WasReference = true, + InnerIsConst = referenceType.PointeeType.IsLocalConstQualified }; if (referenceType is RValueReferenceType) diff --git a/Biohazrd.Transformation/RawTypeTransformationBase.cs b/Biohazrd.Transformation/RawTypeTransformationBase.cs index de93e34..ac20e37 100644 --- a/Biohazrd.Transformation/RawTypeTransformationBase.cs +++ b/Biohazrd.Transformation/RawTypeTransformationBase.cs @@ -42,7 +42,7 @@ protected sealed override TransformationResult Transform(TransformationContext c => declaration switch { ICustomTranslatedDeclaration customDeclaration => customDeclaration.TransformTypeChildren(this, context.Add(declaration)), - TranslatedConstant { Type: not null } constantDeclaration => TransformConstantTypeReferences(context.Add(declaration), constantDeclaration), + TranslatedConstant { Type: not null } constantDeclaration => TransformConstantTypeReferences(context.Add(declaration), constantDeclaration), TranslatedEnum enumDeclaration => TransformEnumTypeReferences(context.Add(declaration), enumDeclaration), TranslatedFunction functionDeclaration => TransformFunctionTypeReferences(context.Add(declaration), functionDeclaration), TranslatedParameter parameterDeclaration => TransformParameterTypeReferences(context.Add(declaration), parameterDeclaration), diff --git a/Biohazrd/#Declarations/TranslatedDeclaration.cs b/Biohazrd/#Declarations/TranslatedDeclaration.cs index f100539..777e992 100644 --- a/Biohazrd/#Declarations/TranslatedDeclaration.cs +++ b/Biohazrd/#Declarations/TranslatedDeclaration.cs @@ -143,6 +143,20 @@ internal bool IsTranslationOf(Decl declaration) return false; } + public bool MatchesId(DeclarationId id) + { + if (Id == id) + { return true; } + + foreach (DeclarationId replacedId in ReplacedIds) + { + if (replacedId == id) + { return true; } + } + + return false; + } + /// Do not use, this is an implementation detail of . internal static TranslatedDeclaration _CreateUniqueClone(TranslatedDeclaration target) => target with diff --git a/Biohazrd/#TypeReferences/ClangDeclTranslatedTypeReference.cs b/Biohazrd/#TypeReferences/ClangDeclTranslatedTypeReference.cs index a81ce26..4acb849 100644 --- a/Biohazrd/#TypeReferences/ClangDeclTranslatedTypeReference.cs +++ b/Biohazrd/#TypeReferences/ClangDeclTranslatedTypeReference.cs @@ -17,5 +17,8 @@ internal ClangDeclTranslatedTypeReference(Decl clangDecl) public override string ToString() => $"`Ref resolved by {ClangDecl}{ToStringSuffix}`"; + + internal override bool __HACK__CouldResolveTo(TranslatedDeclaration declaration) + => declaration.IsTranslationOf(ClangDecl); } } diff --git a/Biohazrd/#TypeReferences/DeclarationIdTranslatedTypeReference.cs b/Biohazrd/#TypeReferences/DeclarationIdTranslatedTypeReference.cs index d6c783f..2a0a0eb 100644 --- a/Biohazrd/#TypeReferences/DeclarationIdTranslatedTypeReference.cs +++ b/Biohazrd/#TypeReferences/DeclarationIdTranslatedTypeReference.cs @@ -15,5 +15,8 @@ internal DeclarationIdTranslatedTypeReference(DeclarationId id) public override string ToString() => $"`Ref resolved by {Id}{ToStringSuffix}`"; + + internal override bool __HACK__CouldResolveTo(TranslatedDeclaration declaration) + => declaration.MatchesId(Id); } } diff --git a/Biohazrd/#TypeReferences/PointerTypeReference.cs b/Biohazrd/#TypeReferences/PointerTypeReference.cs index 558b328..9832421 100644 --- a/Biohazrd/#TypeReferences/PointerTypeReference.cs +++ b/Biohazrd/#TypeReferences/PointerTypeReference.cs @@ -9,6 +9,9 @@ public record PointerTypeReference : TypeReference /// True when this pointer represents what used to be a C++-style reference type. public bool WasReference { get; init; } + /// True when this pointer or reference points to a type which is const qualified. + public bool InnerIsConst { get; init; } + public PointerTypeReference(TypeReference inner) => Inner = inner; diff --git a/Biohazrd/#TypeReferences/PreResolvedTypeReference.cs b/Biohazrd/#TypeReferences/PreResolvedTypeReference.cs index b046aeb..69ceb97 100644 --- a/Biohazrd/#TypeReferences/PreResolvedTypeReference.cs +++ b/Biohazrd/#TypeReferences/PreResolvedTypeReference.cs @@ -41,5 +41,8 @@ public PreResolvedTypeReference(VisitorContext context, TranslatedDeclaration de public override string ToString() => $"`Pre-resolved reference to {Declaration.Name}`"; + + internal override bool __HACK__CouldResolveTo(TranslatedDeclaration declaration) + => ReferenceEquals(declaration, Declaration); } } diff --git a/Biohazrd/#TypeReferences/TranslatedTypeReference.cs b/Biohazrd/#TypeReferences/TranslatedTypeReference.cs index 3df0201..e9a7e60 100644 --- a/Biohazrd/#TypeReferences/TranslatedTypeReference.cs +++ b/Biohazrd/#TypeReferences/TranslatedTypeReference.cs @@ -43,5 +43,8 @@ public static TranslatedTypeReference Create(TranslatedDeclaration declaration) else { return new DeclarationIdTranslatedTypeReference(declaration.Id); } } + + // This is used for a workaround to https://github.com/MochiLibraries/Biohazrd/issues/239 + internal abstract bool __HACK__CouldResolveTo(TranslatedDeclaration declaration); } } diff --git a/Biohazrd/Biohazrd.csproj b/Biohazrd/Biohazrd.csproj index b675d5e..5b7b6d5 100644 --- a/Biohazrd/Biohazrd.csproj +++ b/Biohazrd/Biohazrd.csproj @@ -58,6 +58,8 @@ + + diff --git a/Biohazrd/DeclarationId.cs b/Biohazrd/DeclarationId.cs index eeae79d..b291f9e 100644 --- a/Biohazrd/DeclarationId.cs +++ b/Biohazrd/DeclarationId.cs @@ -34,5 +34,7 @@ public static DeclarationId NewId() ulong newId = Interlocked.Increment(ref NextId); return new DeclarationId(newId); } + + public static DeclarationId Null => default; } } diff --git a/Biohazrd/DeclarationMetadata.cs b/Biohazrd/DeclarationMetadata.cs index 5ed697e..5819518 100644 --- a/Biohazrd/DeclarationMetadata.cs +++ b/Biohazrd/DeclarationMetadata.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Runtime.CompilerServices; @@ -30,6 +31,15 @@ public bool TryGet(out T value) } } + public T Get() + where T : struct, IDeclarationMetadataItem + { + if (!TryGet(out T result)) + { throw new KeyNotFoundException($"No metadata for '{typeof(T).FullName}' exists in the collection."); } + + return result; + } + public bool Has() where T : struct, IDeclarationMetadataItem => Metadata is not null && Metadata.ContainsKey(typeof(T)); diff --git a/Biohazrd/DeclarationReference.cs b/Biohazrd/DeclarationReference.cs index 2f0ac12..4af8d29 100644 --- a/Biohazrd/DeclarationReference.cs +++ b/Biohazrd/DeclarationReference.cs @@ -42,5 +42,8 @@ public DeclarationReference(TranslatedDeclaration declaration) public override string ToString() => Reference.ToString(); + + internal bool __HACK__CouldResolveTo(TranslatedDeclaration declaration) + => Reference.__HACK__CouldResolveTo(declaration); } } diff --git a/Tests/Biohazrd.CSharp.Tests/CSharpTranslationVerifierTrampolineTests.cs b/Tests/Biohazrd.CSharp.Tests/CSharpTranslationVerifierTrampolineTests.cs new file mode 100644 index 0000000..d889c0a --- /dev/null +++ b/Tests/Biohazrd.CSharp.Tests/CSharpTranslationVerifierTrampolineTests.cs @@ -0,0 +1,622 @@ +using Biohazrd.CSharp.Trampolines; +using Biohazrd.Expressions; +using Biohazrd.Tests.Common; +using Biohazrd.Transformation.Common; +using System.Linq; +using Xunit; + +namespace Biohazrd.CSharp.Tests; + +public sealed class CSharpTranslationVerifierTrampolineTests : BiohazrdTestBase +{ + private const string FunctionName = "MyFunction"; + private TranslatedLibrary MakeDefaultTestLibrary(bool createTrampolines = true) + { + TranslatedLibrary library = CreateLibrary + (@" +class MyClass +{ +public: + int MyFunction(int a = 100, int b = 200); +}; +" + ); + + library = new CSharpTypeReductionTransformation().Transform(library); + + if (createTrampolines) + { library = new CreateTrampolinesTransformation().Transform(library); } + + return library; + } + + [Fact] + public void WarnOnMissingTrampolineCollection() + { + TranslatedLibrary library = MakeDefaultTestLibrary(createTrampolines: false); + Assert.Empty(library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics); + library = new CSharpTranslationVerifier().Transform(library); + Assert.Contains + ( + library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("Function does not have trampolines") + ); + } + + [Fact] + public void WarnOnDefaultedTrampolineCollection() + { + TranslatedLibrary library = MakeDefaultTestLibrary(createTrampolines: false); + Assert.Empty(library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => d with { Metadata = d.Metadata.Add(default) } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + Assert.Contains + ( + library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("Function has trampolines but they were defaulted.") + ); + } + + [Fact] + public void NoDiagnosticsForDefaultTrampoline() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new CSharpTranslationVerifier().Transform(library); + Assert.Empty(library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics); + } + + [Fact] + public void WarnOnNameChange() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => d with { Name = "RennamedFunction" } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + Assert.Contains + ( + library.FindDeclaration().FindDeclaration("RennamedFunction").Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("Function's name was changed") + ); + } + + [Fact] + public void WarnOnReturnTypeChange() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => d with { ReturnType = CSharpBuiltinType.UInt } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + Assert.Contains + ( + library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("Function's return type was changed") + ); + } + + [Fact] + public void WarnOnParametersRemoved() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => d with { Parameters = d.Parameters.RemoveAt(0) } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + Assert.Contains + ( + library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("The number of parameters changed") + ); + } + + [Fact] + public void WarnOnParametersAdded() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => d with { Parameters = d.Parameters.Add(d.Parameters[0]) } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + Assert.Contains + ( + library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("The number of parameters changed") + ); + } + + [Fact] + public void WarnOnParameterTypeChanged() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => d with { Parameters = d.Parameters.SetItem(0, d.Parameters[0] with { Type = CSharpBuiltinType.UInt }) } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + Assert.Empty(library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics); // The diagnostic should be on the parameter + Assert.Contains + ( + library.FindDeclaration().FindDeclaration(FunctionName).Parameters[0].Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("Parameter's type was changed") + ); + } + + [Fact] + public void WarnOnParameterNameChanged() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => d with { Parameters = d.Parameters.SetItem(0, d.Parameters[0] with { Name = "newName" }) } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + Assert.Empty(library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics); // The diagnostic should be on the parameter + Assert.Contains + ( + library.FindDeclaration().FindDeclaration(FunctionName).Parameters[0].Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("Parameter's name was changed") + ); + } + + [Fact] + public void WarnOnParameterDefaultValueChanged() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => d with { Parameters = d.Parameters.SetItem(0, d.Parameters[0] with { DefaultValue = IntegerConstant.FromInt32(0xC0FFEE) }) } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + Assert.Empty(library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics); // The diagnostic should be on the parameter + Assert.Contains + ( + library.FindDeclaration().FindDeclaration(FunctionName).Parameters[0].Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("Parameter's default value was changed") + ); + } + + [Fact] + public void NoDiagnosticsForCustomTrampoline() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => + { + TrampolineCollection trampolines = d.Metadata.Get(); + Trampoline primary = trampolines.PrimaryTrampoline; + TrampolineBuilder builder = new(primary, useAsTemplate: false) + { + Name = $"{primary.Name}_UInt", + Description = "Test Trampoline" + }; + builder.AdaptReturnValue(new CastReturnAdapter(trampolines.NativeFunction.ReturnAdapter, CSharpBuiltinType.UInt, CastKind.Explicit)); + return d.WithSecondaryTrampoline(builder.Create()); + } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + Assert.Empty(library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics); + } + + [Fact] + public void NoDiagnosticsForCustomTemplateTrampoline() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => + { + TrampolineCollection trampolines = d.Metadata.Get(); + Trampoline primary = trampolines.PrimaryTrampoline; + TrampolineBuilder builder = new(primary, useAsTemplate: true) + { + Name = $"{primary.Name}_UInt", + Description = "Test Trampoline" + }; + builder.AdaptReturnValue(new CastReturnAdapter(primary.ReturnAdapter, CSharpBuiltinType.UInt, CastKind.Explicit)); + return d.WithSecondaryTrampoline(builder.Create()); + } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + Assert.Empty(library.FindDeclaration().FindDeclaration(FunctionName).Diagnostics); + } + + [Fact] + public void BrokenTrampolineGraphPrimaryLeaf() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => + { + TrampolineCollection trampolines = d.Metadata.Get(); + Trampoline primary = trampolines.PrimaryTrampoline; + TrampolineBuilder builder = new(primary, useAsTemplate: false) + { + Name = $"{primary.Name}_UInt", + Description = "Test Trampoline" + }; + builder.AdaptReturnValue(new CastReturnAdapter(trampolines.NativeFunction.ReturnAdapter, CSharpBuiltinType.UInt, CastKind.Explicit)); + return d with + { + Metadata = d.Metadata.Set(trampolines with + { + // The new trampoline is referencing the trampoline which it is replacing + PrimaryTrampoline = builder.Create() + }) + }; + } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + TranslatedFunction function = library.FindDeclaration().FindDeclaration(FunctionName); + TrampolineCollection trampolineCollection = function.Metadata.Get(); + Assert.ReferenceEqual(trampolineCollection.NativeFunction, trampolineCollection.PrimaryTrampoline); // The broken trampoline should have been removed + Assert.Empty(trampolineCollection.SecondaryTrampolines); + Assert.Contains + ( + function.Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("Trampoline 'Test Trampoline' referenced missing trampoline") + ); + } + + [Fact] + public void BrokenTrampolineGraphSecondaryLeaf() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => + { + TrampolineCollection trampolines = d.Metadata.Get(); + Trampoline primary = trampolines.PrimaryTrampoline; + TrampolineBuilder builder = new(primary, useAsTemplate: false) + { + Name = $"{primary.Name}_UInt", + Description = "Test Trampoline" + }; + builder.AdaptReturnValue(new CastReturnAdapter(trampolines.NativeFunction.ReturnAdapter, CSharpBuiltinType.UInt, CastKind.Explicit)); + return d with + { + Metadata = d.Metadata.Set(trampolines with + { + // Remove the primary trampoline + PrimaryTrampoline = trampolines.NativeFunction, + + // Add the new trampoline we just created + SecondaryTrampolines = trampolines.SecondaryTrampolines.Add(builder.Create()) + }) + }; + } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + TranslatedFunction function = library.FindDeclaration().FindDeclaration(FunctionName); + TrampolineCollection trampolineCollection = function.Metadata.Get(); + Assert.Empty(trampolineCollection.SecondaryTrampolines); // The broken trampoline should have been removed + Assert.ReferenceEqual(trampolineCollection.NativeFunction, trampolineCollection.PrimaryTrampoline); + Assert.Contains + ( + function.Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("Trampoline 'Test Trampoline' referenced missing trampoline") + ); + } + + [Fact] + public void BrokenTrampolineGraphIndirect() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => + { + TrampolineCollection trampolines = d.Metadata.Get(); + Trampoline primary = trampolines.PrimaryTrampoline; + + TrampolineBuilder builder1 = new(primary, useAsTemplate: false) + { + Name = $"{primary.Name}_UInt", + Description = "Test Trampoline" + }; + builder1.AdaptReturnValue(new CastReturnAdapter(trampolines.NativeFunction.ReturnAdapter, CSharpBuiltinType.UInt, CastKind.Explicit)); + Trampoline trampoline1 = builder1.Create(); + + TrampolineBuilder builder2 = new(trampoline1, useAsTemplate: false) + { + Name = $"{primary.Name}_ULong", + Description = "Test Trampoline 2" + }; + builder2.AdaptReturnValue(new CastReturnAdapter(trampolines.NativeFunction.ReturnAdapter, CSharpBuiltinType.ULong, CastKind.Explicit)); + Trampoline trampoline2 = builder2.Create(); + + return d with + { + Metadata = d.Metadata.Set(trampolines with + { + // Remove the primary trampoline + PrimaryTrampoline = trampolines.NativeFunction, + + // Add the new trampolines we just created + SecondaryTrampolines = trampolines.SecondaryTrampolines.Add(trampoline1).Add(trampoline2) + }) + }; + } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + TranslatedFunction function = library.FindDeclaration().FindDeclaration(FunctionName); + TrampolineCollection trampolineCollection = function.Metadata.Get(); + Assert.Empty(trampolineCollection.SecondaryTrampolines); // The broken trampolines should have been removed + Assert.ReferenceEqual(trampolineCollection.NativeFunction, trampolineCollection.PrimaryTrampoline); + Assert.Contains + ( + function.Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("Trampoline 'Test Trampoline' referenced missing trampoline") + ); + Assert.Contains + ( + function.Diagnostics, + // Trampoline 2 referenced Trampoline 1 which was removed, so it should've been removed too + d => d.Severity is Severity.Warning && d.Message.StartsWith("Trampoline 'Test Trampoline 2' referenced missing trampoline") + ); + } + + [Fact] + public void BrokenTrampolineGraphMiddleMissing() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => + { + TrampolineCollection trampolines = d.Metadata.Get(); + Trampoline primary = trampolines.PrimaryTrampoline; + + TrampolineBuilder builder1 = new(primary, useAsTemplate: false) + { + Name = $"{primary.Name}_UInt", + Description = "Test Trampoline" + }; + builder1.AdaptReturnValue(new CastReturnAdapter(trampolines.NativeFunction.ReturnAdapter, CSharpBuiltinType.UInt, CastKind.Explicit)); + Trampoline trampoline1 = builder1.Create(); + + TrampolineBuilder builder2 = new(trampoline1, useAsTemplate: false) + { + Name = $"{primary.Name}_ULong", + Description = "Test Trampoline 2" + }; + builder2.AdaptReturnValue(new CastReturnAdapter(trampolines.NativeFunction.ReturnAdapter, CSharpBuiltinType.ULong, CastKind.Explicit)); + Trampoline trampoline2 = builder2.Create(); + + return d with + { + Metadata = d.Metadata.Set(trampolines with + { + // Remove the primary trampoline + PrimaryTrampoline = trampolines.NativeFunction, + + // Add the new trampoline we just created (just the second though, leave out the first.) + SecondaryTrampolines = trampolines.SecondaryTrampolines.Add(trampoline2) + }) + }; + } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + TranslatedFunction function = library.FindDeclaration().FindDeclaration(FunctionName); + TrampolineCollection trampolineCollection = function.Metadata.Get(); + Assert.Empty(trampolineCollection.SecondaryTrampolines); // The broken trampolines should have been removed + Assert.ReferenceEqual(trampolineCollection.NativeFunction, trampolineCollection.PrimaryTrampoline); + Assert.Contains + ( + function.Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("Trampoline 'Test Trampoline 2' referenced missing trampoline") + ); + } + + [Fact] + public void RemovingTemplateDoesNotBreakGraph() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => + { + TrampolineCollection trampolines = d.Metadata.Get(); + Trampoline primary = trampolines.PrimaryTrampoline; + TrampolineBuilder builder = new(primary, useAsTemplate: true) + { + Name = $"{primary.Name}_UInt", + Description = "Test Trampoline" + }; + builder.AdaptReturnValue(new CastReturnAdapter(trampolines.NativeFunction.ReturnAdapter, CSharpBuiltinType.UInt, CastKind.Explicit)); + return d with + { + Metadata = d.Metadata.Set(trampolines with + { + // The new trampoline is referencing the trampoline which it is replacing + PrimaryTrampoline = builder.Create() + }) + }; + } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + TranslatedFunction function = library.FindDeclaration().FindDeclaration(FunctionName); + TrampolineCollection trampolineCollection = function.Metadata.Get(); + Assert.Equal("Test Trampoline", trampolineCollection.PrimaryTrampoline.Description); + Assert.Empty(trampolineCollection.SecondaryTrampolines); + Assert.Empty(function.Diagnostics); + } + + [Fact] + public void SecondaryRedundantToPrimaryIsRemoved() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => + { + TrampolineCollection trampolines = d.Metadata.Get(); + return d with + { + Metadata = d.Metadata.Set(trampolines.WithTrampoline(trampolines.PrimaryTrampoline)) + }; + } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + TranslatedFunction function = library.FindDeclaration().FindDeclaration(FunctionName); + TrampolineCollection trampolineCollection = function.Metadata.Get(); + Assert.Empty(trampolineCollection.SecondaryTrampolines); + Assert.Contains + ( + function.Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith($"Trampoline '{trampolineCollection.PrimaryTrampoline.Description}' was added as both a secondary and the primary trampoline.") + ); + } + + [Fact] + public void MultipleSecondariesRedundantToPrimaryAreRemoved() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => + { + TrampolineCollection trampolines = d.Metadata.Get(); + return d with + { + Metadata = d.Metadata.Set(trampolines.WithTrampoline(trampolines.PrimaryTrampoline).WithTrampoline(trampolines.PrimaryTrampoline)) + }; + } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + TranslatedFunction function = library.FindDeclaration().FindDeclaration(FunctionName); + TrampolineCollection trampolineCollection = function.Metadata.Get(); + Assert.Empty(trampolineCollection.SecondaryTrampolines); + int count = function.Diagnostics.Count(d => d.Severity is Severity.Warning && d.Message.StartsWith($"Trampoline '{trampolineCollection.PrimaryTrampoline.Description}' was added as both a secondary and the primary trampoline.")); + Assert.Equal(2, count); + } + + [Fact] + public void SecondaryRedundantToSecondaryIsRemoved() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => + { + TrampolineCollection trampolines = d.Metadata.Get(); + Trampoline primary = trampolines.PrimaryTrampoline; + + TrampolineBuilder builder1 = new(primary, useAsTemplate: true) + { + Name = $"{primary.Name}_UInt", + Description = "Test Trampoline" + }; + builder1.AdaptReturnValue(new CastReturnAdapter(trampolines.NativeFunction.ReturnAdapter, CSharpBuiltinType.UInt, CastKind.Explicit)); + Trampoline trampoline1 = builder1.Create(); + + TrampolineBuilder builder2 = new(primary, useAsTemplate: true) + { + Name = $"{primary.Name}_ULong", + Description = "Test Trampoline 2" + }; + builder2.AdaptReturnValue(new CastReturnAdapter(trampolines.NativeFunction.ReturnAdapter, CSharpBuiltinType.ULong, CastKind.Explicit)); + Trampoline trampoline2 = builder2.Create(); + + return d with + { + Metadata = d.Metadata.Set(trampolines with + { + // Remove the primary trampoline + PrimaryTrampoline = trampolines.NativeFunction, + + // Add the new trampoline we just created (adding an extra trampoline1) + SecondaryTrampolines = trampolines.SecondaryTrampolines.Add(trampoline1).Add(trampoline2).Add(trampoline1) + }) + }; + } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + TranslatedFunction function = library.FindDeclaration().FindDeclaration(FunctionName); + TrampolineCollection trampolineCollection = function.Metadata.Get(); + Assert.Single(trampolineCollection.SecondaryTrampolines, t => t.Description == "Test Trampoline"); + Assert.Single(trampolineCollection.SecondaryTrampolines, t => t.Description == "Test Trampoline 2"); + Assert.Contains + ( + function.Diagnostics, + d => d.Severity is Severity.Warning && d.Message.StartsWith("Trampoline 'Test Trampoline' was added to the secondary trampoline list more than once.") + ); + } + + [Fact] + public void MultipleSecondariesRedundantToSecondaryAreRemoved() + { + TranslatedLibrary library = MakeDefaultTestLibrary(); + library = new SimpleTransformation() + { + TransformFunction = (c, d) => + { + TrampolineCollection trampolines = d.Metadata.Get(); + Trampoline primary = trampolines.PrimaryTrampoline; + + TrampolineBuilder builder1 = new(primary, useAsTemplate: true) + { + Name = $"{primary.Name}_UInt", + Description = "Test Trampoline" + }; + builder1.AdaptReturnValue(new CastReturnAdapter(trampolines.NativeFunction.ReturnAdapter, CSharpBuiltinType.UInt, CastKind.Explicit)); + Trampoline trampoline1 = builder1.Create(); + + TrampolineBuilder builder2 = new(primary, useAsTemplate: true) + { + Name = $"{primary.Name}_ULong", + Description = "Test Trampoline 2" + }; + builder2.AdaptReturnValue(new CastReturnAdapter(trampolines.NativeFunction.ReturnAdapter, CSharpBuiltinType.ULong, CastKind.Explicit)); + Trampoline trampoline2 = builder2.Create(); + + return d with + { + Metadata = d.Metadata.Set(trampolines with + { + // Remove the primary trampoline + PrimaryTrampoline = trampolines.NativeFunction, + + // Add the new trampoline we just created (adding an extra trampoline1) + SecondaryTrampolines = trampolines.SecondaryTrampolines.Add(trampoline1).Add(trampoline1).Add(trampoline2).Add(trampoline1) + }) + }; + } + }.Transform(library); + library = new CSharpTranslationVerifier().Transform(library); + + TranslatedFunction function = library.FindDeclaration().FindDeclaration(FunctionName); + TrampolineCollection trampolineCollection = function.Metadata.Get(); + Assert.Single(trampolineCollection.SecondaryTrampolines, t => t.Description == "Test Trampoline"); + Assert.Single(trampolineCollection.SecondaryTrampolines, t => t.Description == "Test Trampoline 2"); + int count = function.Diagnostics.Count(d => d.Severity is Severity.Warning && d.Message.StartsWith("Trampoline 'Test Trampoline' was added to the secondary trampoline list more than once.")); + Assert.Equal(2, count); + } +} diff --git a/Tests/Biohazrd.CSharp.Tests/TrampolineCollectionTests.cs b/Tests/Biohazrd.CSharp.Tests/TrampolineCollectionTests.cs new file mode 100644 index 0000000..54c245e --- /dev/null +++ b/Tests/Biohazrd.CSharp.Tests/TrampolineCollectionTests.cs @@ -0,0 +1,423 @@ +using Biohazrd.CSharp.Trampolines; +using Biohazrd.Tests.Common; +using System; +using System.Collections.Generic; +using Xunit; + +namespace Biohazrd.CSharp.Tests; + +public sealed class TrampolineCollectionTests : BiohazrdTestBase +{ + private TrampolineCollection CreateCollection() + => CreateCollection(out _, out _, out _); + + private TrampolineCollection CreateCollection(out TrampolineCollection otherCollection) + => CreateCollection(out _, out otherCollection, out _); + + private TrampolineCollection CreateCollection(out TranslatedFunction function) + => CreateCollection(out function, out _, out _); + + private TrampolineCollection CreateCollection(out TranslatedFunction function, out TrampolineCollection otherCollection, out TranslatedFunction otherFunction) + { + TranslatedLibrary library = CreateLibrary + (@" +int MyFunction(const int& a = 100, const int& b = 200); // MyFunction will always have a default primary trampoline +int OtherFunction(int a = 100, int b = 200); // OtherFunction will never have a default primary trampoline +" + ); + + library = new CSharpTypeReductionTransformation().Transform(library); + library = new CreateTrampolinesTransformation().Transform(library); + + // Get and sanity check the main function + TrampolineCollection collection; + { + function = library.FindDeclaration("MyFunction"); + collection = function.Metadata.Get(); + + Assert.NotNull(collection.NativeFunction); + Assert.True(collection.NativeFunction.IsNativeFunction); + Assert.Equal(function.Id, collection.NativeFunction.TargetFunctionId); + + Assert.NotNull(collection.PrimaryTrampoline); + Assert.False(collection.PrimaryTrampoline.IsNativeFunction); + Assert.ReferenceEqual(collection.NativeFunction, collection.PrimaryTrampoline.Target); + Assert.Equal(collection.NativeFunction.TargetFunctionId, collection.PrimaryTrampoline.TargetFunctionId); + + Assert.Empty(collection.SecondaryTrampolines); + } + + // Get and sanity check the other function + { + otherFunction = library.FindDeclaration("OtherFunction"); + otherCollection = otherFunction.Metadata.Get(); + + Assert.NotNull(otherCollection.NativeFunction); + Assert.True(otherCollection.NativeFunction.IsNativeFunction); + Assert.Equal(otherFunction.Id, otherCollection.NativeFunction.TargetFunctionId); + + // The other function does not require a primary trampoline so the native dummy trampoline should be the primary + Assert.NotNull(otherCollection.PrimaryTrampoline); + Assert.ReferenceEqual(otherCollection.NativeFunction, otherCollection.PrimaryTrampoline); + Assert.Equal(otherCollection.NativeFunction.TargetFunctionId, otherCollection.PrimaryTrampoline.TargetFunctionId); + + Assert.Empty(otherCollection.SecondaryTrampolines); + } + + return collection; + } + + [Fact] + public void NativeFunctionCanBeReplaced() + { + TrampolineCollection collection = CreateCollection(); + collection = collection with + { + NativeFunction = collection.NativeFunction with { Name = "NewName" } + }; + Assert.Equal("NewName", collection.NativeFunction.Name); + Assert.True(collection.NativeFunction.IsNativeFunction); + } + + [Fact] + public void NativeFunctionCannotBeSetToNonNative() + { + TrampolineCollection collection = CreateCollection(); + Assert.Throws + ( + () => collection = collection with { NativeFunction = collection.PrimaryTrampoline } + ); + } + + [Fact] + public void NativeFunctionCannotBeUnrelated() + { + TrampolineCollection collection = CreateCollection(out TrampolineCollection otherCollection); + Assert.Throws + ( + () => collection = collection with { NativeFunction = otherCollection.NativeFunction } + ); + } + + [Fact] + public void NativeFunctionCannotBeNull() + { + TrampolineCollection collection = CreateCollection(out TrampolineCollection otherCollection); + Assert.ThrowsAny + ( + () => collection = collection with { NativeFunction = null! } + ); + } + + [Fact] + public void PrimaryTrampolineCanBeReplaced() + { + TrampolineCollection collection = CreateCollection(); + collection = collection with + { + PrimaryTrampoline = collection.PrimaryTrampoline with { Name = "NewName" } + }; + Assert.Equal("NewName", collection.PrimaryTrampoline.Name); + } + + [Fact] + public void PrimaryTrampolineCannotBeUnrelated() + { + TrampolineCollection collection = CreateCollection(out TrampolineCollection otherCollection); + Assert.Throws + ( + () => collection = collection with { PrimaryTrampoline = otherCollection.PrimaryTrampoline } + ); + } + + [Fact] + public void PrimaryTrampolineCanBeNative() + { + TrampolineCollection collection = CreateCollection(); + collection = collection with + { + PrimaryTrampoline = collection.NativeFunction + }; + Assert.True(collection.PrimaryTrampoline.IsNativeFunction); + } + + [Fact] + public void PrimaryTrampolineCannotBeNull() + { + TrampolineCollection collection = CreateCollection(out TrampolineCollection otherCollection); + Assert.ThrowsAny + ( + () => collection = collection with { PrimaryTrampoline = null! } + ); + } + + [Fact] + public void PrimaryTrampolineDoesNotVerifyGraph() + { + TrampolineCollection collection = CreateCollection(out TrampolineCollection otherCollection); + TrampolineBuilder builder = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName" + }; + + // This isn't a valid collection, but the enforcement of the graph completedness is handled by the verification stage + collection = collection with { PrimaryTrampoline = builder.Create() }; + Assert.Equal("AlternateName", collection.PrimaryTrampoline.Name); + Assert.NotNull(collection.PrimaryTrampoline.Target); + Assert.False(collection.Contains(collection.PrimaryTrampoline.Target)); + } + + [Fact] + public void SecondaryTrampolinesCanBeReplaced() + { + TrampolineCollection collection = CreateCollection(); + TrampolineBuilder builder = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName" + }; + Trampoline secondary = builder.Create(); + + collection = collection with { SecondaryTrampolines = collection.SecondaryTrampolines.Add(secondary) }; + Assert.Contains(secondary, collection.SecondaryTrampolines); + } + + [Fact] + public void SecondaryTrampolinesCanBeReplacedUsingHelper() + { + TrampolineCollection collection = CreateCollection(); + TrampolineBuilder builder = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName" + }; + Trampoline secondary = builder.Create(); + + collection = collection.WithTrampoline(secondary); + Assert.Contains(secondary, collection.SecondaryTrampolines); + } + + [Fact] + public void SecondaryTrampolinesCannotBeUnrelated() + { + TrampolineCollection collection = CreateCollection(out TrampolineCollection otherCollection); + TrampolineBuilder builder = new(otherCollection.NativeFunction, useAsTemplate: false) { Name = "AlternateName" }; + Trampoline trampoline = builder.Create(); + Assert.False(trampoline.IsNativeFunction); + + Exception ex = Assert.Throws + ( + () => collection = collection with { SecondaryTrampolines = collection.SecondaryTrampolines.Add(trampoline) } + ); + Assert.Contains($"'{trampoline}' does not belong", ex.Message); + } + + [Fact] + public void SecondaryTrampolinesCannotBeUnrelatedUsingHelper() + { + TrampolineCollection collection = CreateCollection(out TrampolineCollection otherCollection); + TrampolineBuilder builder = new(otherCollection.NativeFunction, useAsTemplate: false) { Name = "AlternateName" }; + Trampoline trampoline = builder.Create(); + Assert.False(trampoline.IsNativeFunction); + + Exception ex = Assert.Throws + ( + () => collection = collection.WithTrampoline(trampoline) + ); + Assert.Contains($"does not belong", ex.Message); + } + + [Fact] + public void SecondaryTrampolinesCannotBeNative() + { + TrampolineCollection collection = CreateCollection(); + Exception ex = Assert.Throws + ( + () => collection = collection with { SecondaryTrampolines = collection.SecondaryTrampolines.Add(collection.NativeFunction) } + ); + Assert.Contains("Native functions cannot be trampolines.", ex.Message); + } + + [Fact] + public void SecondaryTrampolinesCannotBeNativeUsingHelper() + { + TrampolineCollection collection = CreateCollection(); + Exception ex = Assert.Throws + ( + () => collection = collection.WithTrampoline(collection.NativeFunction) + ); + Assert.StartsWith("Native trampolines", ex.Message); + } + + [Fact] + public void SecondaryTrampolinesCannotBeNull() + { + TrampolineCollection collection = CreateCollection(); + Assert.ThrowsAny + ( + () => collection = collection with { SecondaryTrampolines = collection.SecondaryTrampolines.Add(null!) } + ); + } + + [Fact] + public void SecondaryTrampolinesCannotBeNullUsingHelper() + { + TrampolineCollection collection = CreateCollection(); + Assert.ThrowsAny + ( + () => collection = collection.WithTrampoline(null!) + ); + } + + [Fact] + public void SecondaryTrampolinesDoesNotVerifyGraph() + { + TrampolineCollection collection = CreateCollection(out TrampolineCollection otherCollection); + TrampolineBuilder builder = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName" + }; + + // This isn't a valid collection, but the enforcement of the graph completedness is handled by the verification stage + collection = collection with { PrimaryTrampoline = collection.NativeFunction }; + collection = collection with { SecondaryTrampolines = collection.SecondaryTrampolines.Add(builder.Create()) }; + Trampoline trampoline = Assert.Single(collection.SecondaryTrampolines, t => t.Name == "AlternateName"); + Assert.NotNull(trampoline.Target); + Assert.False(collection.Contains(trampoline.Target)); + } + + [Fact] + public void SecondaryTrampolinesDoesNotVerifyGraphUsingHelper() + { + TrampolineCollection collection = CreateCollection(out TrampolineCollection otherCollection); + TrampolineBuilder builder = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName" + }; + + // This isn't a valid collection, but the enforcement of the graph completedness is handled by the verification stage + collection = collection with { PrimaryTrampoline = collection.NativeFunction }; + collection = collection.WithTrampoline(builder.Create()); + Trampoline trampoline = Assert.Single(collection.SecondaryTrampolines, t => t.Name == "AlternateName"); + Assert.NotNull(trampoline.Target); + Assert.False(collection.Contains(trampoline.Target)); + } + + [Fact] + public void Contains() + { + TrampolineCollection collection = CreateCollection(out TrampolineCollection otherCollection); + TrampolineBuilder builder = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName" + }; + collection = collection.WithTrampoline(builder.Create()); + + Trampoline a = collection.NativeFunction; + Trampoline b = collection.PrimaryTrampoline; + Trampoline c = Assert.Single(collection.SecondaryTrampolines); + Assert.NotReferenceEqual(a, b); + Assert.NotReferenceEqual(a, c); + Assert.NotReferenceEqual(b, c); + Assert.True(collection.Contains(a)); + Assert.True(collection.Contains(b)); + Assert.True(collection.Contains(c)); + Assert.False(otherCollection.Contains(a)); + Assert.False(otherCollection.Contains(b)); + Assert.False(otherCollection.Contains(c)); + } + + [Fact] + public void Enumerator() + { + TrampolineCollection collection = CreateCollection(); + TrampolineBuilder builder1 = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName" + }; + collection = collection.WithTrampoline(builder1.Create()); + TrampolineBuilder builder2 = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName2" + }; + collection = collection.WithTrampoline(builder2.Create()); + + List trampolines = new(); + foreach (Trampoline trampoline in collection) + { trampolines.Add(trampoline); } + + Assert.Equal(4, trampolines.Count); + Assert.ReferenceEqual(collection.NativeFunction, trampolines[0]); + Assert.ReferenceEqual(collection.PrimaryTrampoline, trampolines[1]); + Assert.ReferenceEqual(collection.SecondaryTrampolines[0], trampolines[2]); + Assert.ReferenceEqual(collection.SecondaryTrampolines[1], trampolines[3]); + } + + [Fact] + public void Enumerator_NoPrimary() + { + CreateCollection(out TrampolineCollection collection); + TrampolineBuilder builder1 = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName" + }; + collection = collection.WithTrampoline(builder1.Create()); + TrampolineBuilder builder2 = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName2" + }; + collection = collection.WithTrampoline(builder2.Create()); + + List trampolines = new(); + foreach (Trampoline trampoline in collection) + { trampolines.Add(trampoline); } + + Assert.Equal(3, trampolines.Count); + Assert.ReferenceEqual(collection.NativeFunction, trampolines[0]); + Assert.ReferenceEqual(collection.SecondaryTrampolines[0], trampolines[1]); + Assert.ReferenceEqual(collection.SecondaryTrampolines[1], trampolines[2]); + } + + [Fact] + public void IEnumerator() + { + TrampolineCollection collection = CreateCollection(); + TrampolineBuilder builder1 = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName" + }; + collection = collection.WithTrampoline(builder1.Create()); + TrampolineBuilder builder2 = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName2" + }; + collection = collection.WithTrampoline(builder2.Create()); + + List trampolines = new((IEnumerable)collection); + Assert.Equal(4, trampolines.Count); + Assert.ReferenceEqual(collection.NativeFunction, trampolines[0]); + Assert.ReferenceEqual(collection.PrimaryTrampoline, trampolines[1]); + Assert.ReferenceEqual(collection.SecondaryTrampolines[0], trampolines[2]); + Assert.ReferenceEqual(collection.SecondaryTrampolines[1], trampolines[3]); + } + + [Fact] + public void IEnumerator_NoPrimary() + { + CreateCollection(out TrampolineCollection collection); + TrampolineBuilder builder1 = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName" + }; + collection = collection.WithTrampoline(builder1.Create()); + TrampolineBuilder builder2 = new(collection.PrimaryTrampoline, useAsTemplate: false) + { + Name = "AlternateName2" + }; + collection = collection.WithTrampoline(builder2.Create()); + + List trampolines = new((IEnumerable)collection); + Assert.Equal(3, trampolines.Count); + Assert.ReferenceEqual(collection.NativeFunction, trampolines[0]); + Assert.ReferenceEqual(collection.SecondaryTrampolines[0], trampolines[1]); + Assert.ReferenceEqual(collection.SecondaryTrampolines[1], trampolines[2]); + } +}