diff --git a/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/StringMarshaling/LLVMErrorRef.cs b/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/StringMarshaling/LLVMErrorRef.cs index 4a4ca58b3..8d64ecfb9 100644 --- a/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/StringMarshaling/LLVMErrorRef.cs +++ b/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/StringMarshaling/LLVMErrorRef.cs @@ -82,7 +82,7 @@ public LLVMErrorTypeId TypeId } return LazyMessage.IsValueCreated - ? LLVMGetStringErrorTypeId() + ? LLVMGetStringErrorTypeId() // string retrieved already so the "type" is known. : LLVMErrorTypeId.FromABI(LLVMGetErrorTypeId(DangerousGetHandle())); [DllImport( LibraryName )] @@ -135,7 +135,7 @@ public static LLVMErrorRef Create( LazyEncodedString errMsg ) /// In all other cases a fully wrapped handle () is used via . /// /// - public static unsafe nint CreateForNativeOut( LazyEncodedString errMsg ) + public static nint CreateForNativeOut( LazyEncodedString errMsg ) { unsafe { @@ -152,6 +152,14 @@ public static unsafe nint CreateForNativeOut( LazyEncodedString errMsg ) static unsafe extern nint /*LLVMErrorRef*/ LLVMCreateStringError( byte* ErrMsg ); } + /// Create a new as from + /// Exceotion to get the error message from + /// + public static nint CreateForNativeOut( Exception ex) + { + return CreateForNativeOut(ex.Message); + } + public void Dispose() { // if a message was previously realized, dispose of it now. @@ -180,19 +188,6 @@ public static LLVMErrorRef FromABI( nint abiValue ) return new(abiValue); } - private LLVMErrorTypeId LazyGetTypeId() - { - // NOTE: Native API will fail (undocumented) if error message already retrieved. - // This causes a crash in a debugger trying to show this property as ToString() - // is already called. - return LazyMessage.IsValueCreated ? default : LLVMErrorTypeId.FromABI(LLVMGetErrorTypeId(DangerousGetHandle())); - - [DllImport( LibraryName )] - [UnmanagedCallConv( CallConvs = [ typeof( CallConvCdecl ) ] )] - [SuppressMessage( "Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = "Signature is P/Invoke ready" )] - static extern /*LLVMErrorTypeId*/nint LLVMGetErrorTypeId(/*LLVMErrorRef*/ nint Err ); - } - private ErrorMessageString LazyGetMessage( ) { if(IsNull) diff --git a/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/Core.cs b/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/Core.cs index d44606970..bc53f773a 100644 --- a/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/Core.cs +++ b/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/Core.cs @@ -421,7 +421,7 @@ public static partial class Core [LibraryImport( LibraryName )] [UnmanagedCallConv( CallConvs = [ typeof( CallConvCdecl ) ] )] - public static unsafe partial void LLVMContextSetDiagnosticHandler( LLVMContextRefAlias C, LLVMDiagnosticHandler Handler, nint DiagnosticContext ); + public static unsafe partial void LLVMContextSetDiagnosticHandler( LLVMContextRefAlias C, LLVMDiagnosticHandler Handler, void* DiagnosticContext ); [LibraryImport( LibraryName )] [UnmanagedCallConv( CallConvs = [ typeof( CallConvCdecl ) ] )] @@ -429,7 +429,7 @@ public static partial class Core [LibraryImport( LibraryName )] [UnmanagedCallConv( CallConvs = [ typeof( CallConvCdecl ) ] )] - public static unsafe partial nint LLVMContextGetDiagnosticContext( LLVMContextRefAlias C ); + public static unsafe partial void* LLVMContextGetDiagnosticContext( LLVMContextRefAlias C ); [LibraryImport( LibraryName )] [UnmanagedCallConv( CallConvs = [ typeof( CallConvCdecl ) ] )] diff --git a/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/ExecutionEngine.cs b/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/ExecutionEngine.cs index 324d1d73b..47953da8e 100644 --- a/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/ExecutionEngine.cs +++ b/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/ExecutionEngine.cs @@ -10,9 +10,9 @@ namespace Ubiquity.NET.Llvm.Interop.ABI.llvm_c // Misplaced using directive; It isn't misplaced - tooling is too brain dead to know the difference between an alias and a using directive #pragma warning disable IDE0065, SA1200, SA1135 using unsafe LLVMMemoryManagerAllocateCodeSectionCallback = delegate* unmanaged[Cdecl]< void* /*Opaque*/, nuint /*Size*/, uint /*Alignment*/, uint /*SectionID*/, byte* /*SectionName*/, byte* /*retVal*/>; - using unsafe LLVMMemoryManagerAllocateDataSectionCallback = delegate* unmanaged[Cdecl]< void* /*Opaque*/, nuint /*Size*/, uint /*Alignment*/, uint /*SectionID*/, byte* /*SectionName*/, bool /*IsReadOnly*/, byte* /*retVal*/>; + using unsafe LLVMMemoryManagerAllocateDataSectionCallback = delegate* unmanaged[Cdecl]< void* /*Opaque*/, nuint /*Size*/, uint /*Alignment*/, uint /*SectionID*/, byte* /*SectionName*/, /*LLVMBool*/ Int32 /*IsReadOnly*/, byte* /*retVal*/>; using unsafe LLVMMemoryManagerDestroyCallback = delegate* unmanaged[Cdecl]< void* /*Opaque*/, void /*retVal*/ >; - using unsafe LLVMMemoryManagerFinalizeMemoryCallback = delegate* unmanaged[Cdecl]< void* /*Opaque*/, byte** /*ErrMsg*/, bool /*retVal*/>; + using unsafe LLVMMemoryManagerFinalizeMemoryCallback = delegate* unmanaged[Cdecl]< void* /*Opaque*/, byte** /*ErrMsg*/, /*LLVMBool*/ Int32 /*retVal*/>; #pragma warning restore IDE0065, SA1200, SA1135 [StructLayout( LayoutKind.Sequential )] diff --git a/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/Orc.cs b/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/Orc.cs index 5af120550..8b295fb67 100644 --- a/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/Orc.cs +++ b/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/Orc.cs @@ -207,7 +207,7 @@ public static unsafe partial LLVMOrcSymbolStringPoolEntryRef LLVMOrcExecutionSes LazyEncodedString Name ); - [Experimental( "LLVM001" )] + [Experimental( "LLVMEXP001" )] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void LLVMOrcExecutionSessionLookup( LLVMOrcExecutionSessionRef ES, @@ -223,7 +223,7 @@ public static unsafe void LLVMOrcExecutionSessionLookup( [LibraryImport( LibraryName )] [UnmanagedCallConv( CallConvs = [ typeof( CallConvCdecl ) ] )] - [Experimental("LLVMEXP001")] + [Experimental("LLVMEXP002")] private static unsafe partial void LLVMOrcExecutionSessionLookup( LLVMOrcExecutionSessionRef ES, LLVMOrcLookupKind K, diff --git a/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/OrcEE.cs b/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/OrcEE.cs index 5b288b93a..39211da25 100644 --- a/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/OrcEE.cs +++ b/src/Interop/Ubiquity.NET.Llvm.Interop/ABI/llvm-c/OrcEE.cs @@ -6,10 +6,10 @@ namespace Ubiquity.NET.Llvm.Interop.ABI.llvm_c // Misplaced using directive; It isn't misplaced - tooling is too brain dead to know the difference between an alias and a using directive #pragma warning disable IDE0065, SA1200, SA1135 using unsafe LLVMMemoryManagerAllocateCodeSectionCallback = delegate* unmanaged[Cdecl]< void* /*Opaque*/, nuint /*Size*/, uint /*Alignment*/, uint /*SectionID*/, byte* /*SectionName*/, byte* /*retVal*/>; - using unsafe LLVMMemoryManagerAllocateDataSectionCallback = delegate* unmanaged[Cdecl]< void* /*Opaque*/, nuint /*Size*/, uint /*Alignment*/, uint /*SectionID*/, byte* /*SectionName*/, bool /*IsReadOnly*/, byte* /*retVal*/>; + using unsafe LLVMMemoryManagerAllocateDataSectionCallback = delegate* unmanaged[Cdecl]< void* /*Opaque*/, nuint /*Size*/, uint /*Alignment*/, uint /*SectionID*/, byte* /*SectionName*/, /*LLVMBool*/ Int32 /*IsReadOnly*/, byte* /*retVal*/>; using unsafe LLVMMemoryManagerCreateContextCallback = delegate* unmanaged[Cdecl]< void* /*CtxCtx*/, void* /*retVal*/>; using unsafe LLVMMemoryManagerDestroyCallback = delegate* unmanaged[Cdecl]< void* /*Opaque*/, void /*retVal*/ >; - using unsafe LLVMMemoryManagerFinalizeMemoryCallback = delegate* unmanaged[Cdecl]< void* /*Opaque*/, byte** /*ErrMsg*/, bool /*retVal*/>; + using unsafe LLVMMemoryManagerFinalizeMemoryCallback = delegate* unmanaged[Cdecl]< void* /*Opaque*/, byte** /*ErrMsg*/, /*LLVMBool*/ Int32 /*retVal*/>; using unsafe LLVMMemoryManagerNotifyTerminatingCallback = delegate* unmanaged[Cdecl]< void* /*CtxCtx*/, void /*retVal*/>; #pragma warning restore IDE0065, SA1200, SA1135 diff --git a/src/Ubiquity.NET.CommandLine.UT/RawApiTests.cs b/src/Ubiquity.NET.CommandLine.UT/RawApiTests.cs index 68e8ffbd5..99e190636 100644 --- a/src/Ubiquity.NET.CommandLine.UT/RawApiTests.cs +++ b/src/Ubiquity.NET.CommandLine.UT/RawApiTests.cs @@ -2,7 +2,6 @@ // Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. using System.CommandLine; -using System.CommandLine.Help; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/src/Ubiquity.NET.CommandLine/ParseResultExtensions.cs b/src/Ubiquity.NET.CommandLine/ParseResultExtensions.cs index b78e809fa..d97c2dfd4 100644 --- a/src/Ubiquity.NET.CommandLine/ParseResultExtensions.cs +++ b/src/Ubiquity.NET.CommandLine/ParseResultExtensions.cs @@ -6,47 +6,23 @@ using System.CommandLine; using System.CommandLine.Help; using System.CommandLine.Parsing; -using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Ubiquity.NET.CommandLine { + // This does NOT use the new C# 14 extension syntax due to several reasons + // 1) Code lens does not work https://github.com/dotnet/roslyn/issues/79006 [Sadly marked as "not planned" - e.g., dead-end] + // 2) MANY analyzers get things wrong and need to be supressed (CA1000, CA1034, and many others [SAxxxx]) + // 3) Many tools (like docfx don't support the new syntax yet) + // 4) No clear support for Caller* attributes ([CallerArgumentExpression(...)]). + // + // Bottom line it's a good idea with an incomplete implementation lacking support + // in the overall ecosystem. Don't use it unless you absolutely have to until all + // of that is sorted out. + /// Utility extension methods for command line parsing - [SuppressMessage( "Design", "CA1034:Nested types should not be visible", Justification = "BS, extension" )] - [SuppressMessage( "Performance", "CA1822:Mark members as static", Justification = "BS, Extension" )] public static class ParseResultExtensions { -#if DOCFX_BUILD_SUPPORTS_EXTENSION_KEYWORD - extension(ParseResult self) - { - /// Gets a value indicating whether has any errors - public bool HasErrors => self.Errors.Count > 0; - - public HelpOption? HelpOption - { - get - { - var helpOptions = from r in self.CommandResult.RecurseWhileNotNull(r => r.Parent as CommandResult) - from o in r.Command.Options.OfType() - select o; - - return helpOptions.FirstOrDefault(); - } - } - - public VersionOption? VersionOption - { - get - { - var versionOptions = from r in self.CommandResult.RecurseWhileNotNull(r => r.Parent as CommandResult) - from o in r.Command.Options.OfType() - select o; - - return versionOptions.FirstOrDefault(); - } - } - } -#else /// Gets a value indicating whether has any errors /// Result to test for errors /// value indicating whether has any errors @@ -75,7 +51,6 @@ from o in r.Command.Options.OfType() return versionOptions.FirstOrDefault(); } -#endif // shamelessly "borrowed" from: https://github.com/dotnet/dotnet/blob/8c7b3dcd2bd657c11b12973f1214e7c3c616b174/src/command-line-api/src/System.CommandLine/Help/HelpBuilderExtensions.cs#L42 internal static IEnumerable RecurseWhileNotNull( this T? source, Func next ) diff --git a/src/Ubiquity.NET.Extensions/FluentValidationExtensions.cs b/src/Ubiquity.NET.Extensions/FluentValidationExtensions.cs index df4023463..9690dda51 100644 --- a/src/Ubiquity.NET.Extensions/FluentValidationExtensions.cs +++ b/src/Ubiquity.NET.Extensions/FluentValidationExtensions.cs @@ -5,24 +5,31 @@ using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Runtime.CompilerServices; namespace Ubiquity.NET.Extensions { + // This does NOT use the new C# 14 extension syntax due to several reasons + // 1) Code lens does not work https://github.com/dotnet/roslyn/issues/79006 [Sadly, marked as "not planned" - e.g., dead-end] + // 2) MANY analyzers get things wrong and need to be supressed (CA1000, CA1034, and many others [SAxxxx]) + // 3) Many tools (like docfx) don't support the new syntax yet and it isn't clear if they will in the future. + // 4) No clear support for Caller* attributes ([CallerArgumentExpression(...)]). + // + // Bottom line it's a good idea with an incomplete implementation lacking support + // in the overall ecosystem. Don't use it unless you absolutely have to until all + // of that is sorted out. + /// Extension class to provide Fluent validation of arguments /// /// These are similar to many of the built-in support checks except that /// they use a `Fluent' style to allow validation of parameters used as inputs /// to other functions that ultimately produce parameters for a base constructor. /// They also serve to provide validation when using body expressions for property - /// method implementations etc... + /// method implementations etc... Though the C# 14 field keyword makes that + /// use mostly a moot point. /// public static class FluentValidationExtensions { - // NOTE: These DO NOT use the new `extension` keyword syntax as it is not clear - // how CallerArgumentExpression is supposed to be used for those... - /// Throws an exception if is /// Type of reference parameter to test for /// Instance to test @@ -72,9 +79,12 @@ public static T ThrowIfNotDefined( this T self, [CallerArgumentExpression( na where T : struct, Enum { ArgumentNullException.ThrowIfNull(self, exp); + + // TODO: Move the exception message to a resource for globalization + // This matches the overloaded constructor version but allows for reporting enums with non-int underlying type. return Enum.IsDefined( typeof(T), self ) ? self - : throw new InvalidEnumArgumentException(exp); + : throw new InvalidEnumArgumentException($"The value of argument '{exp}' ({self}) is invalid for Enum of type '{typeof(T)}'"); } } } diff --git a/src/Ubiquity.NET.InteropHelpers/MarshalGCHandle.cs b/src/Ubiquity.NET.InteropHelpers/MarshalGCHandle.cs deleted file mode 100644 index 24331da77..000000000 --- a/src/Ubiquity.NET.InteropHelpers/MarshalGCHandle.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Ubiquity.NET Contributors. All rights reserved. -// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -namespace Ubiquity.NET.InteropHelpers -{ - /// Utility class for supporting use of a GCHandle (as a raw native pointer) - /// - /// Many native APIs use a callback with a "Context" as a raw native pointer. It is common in - /// such cases to use a as the context. - /// - public static class MarshalGCHandle - { - /// Try pattern to convert a as a raw native pointer - /// Type of object the context should hold - /// Native pointer for the context - /// Value of the context or default if return is - /// if the is valid and converted to - /// - /// This assumes that is a that was allocated and provided to native code via - /// a call to . This will follow the try pattern to resolve - /// back to the original instance the handle is referencing. This allows managed code callbacks to use managed objects as - /// an opaque "context" value for native APIs. - /// - /// - /// In order to satisfy nullability code analysis, call sites must declare explicitly. Otherwise, - /// it is deduced as the type used for , which will cause analysis to complain if it isn't a nullable type. - /// Thus, without explicit declaration of the type without nullability it is assumed nullable and the - /// is effectively moot. So call sites should always specify the generic type parameter explicitly. - /// - /// - public static unsafe bool TryGet( void* ctx, [MaybeNullWhen( false )] out T value ) - { - if(ctx is not null && GCHandle.FromIntPtr( (nint)ctx ).Target is T self) - { - value = self; - return true; - } - - value = default; - return false; - } - } -} diff --git a/src/Ubiquity.NET.InteropHelpers/NativeContext.cs b/src/Ubiquity.NET.InteropHelpers/NativeContext.cs new file mode 100644 index 000000000..b703679d7 --- /dev/null +++ b/src/Ubiquity.NET.InteropHelpers/NativeContext.cs @@ -0,0 +1,119 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace Ubiquity.NET.InteropHelpers +{ + // This does NOT use the new C# 14 extension syntax due to several reasons + // 1) Code lens does not work https://github.com/dotnet/roslyn/issues/79006 [Sadly marked as "not planned" - e.g., dead-end] + // 2) MANY analyzers get things wrong and need to be supressed (CA1000, CA1034, and many others [SAxxxx]) + // 3) Many tools (like docfx don't support the new syntax yet) + // 4) No clear support for Caller* attributes ([CallerArgumentExpression(...)]). + // + // Bottom line it's a good idea with an incomplete implementation lacking support + // in the overall ecosystem. Don't use it unless you absolutely have to until all + // of that is sorted out. + + /// Operations and extensions for a Native ABI context + /// + /// To interop with native ABI callbacks it is important to ensure that any + /// context pointer provided is valid when the callback is called by native + /// layers. This type provides support for doing so using a . + /// The handle is allocated in and then released + /// in . The actual target is obtainable via + /// which is normally used in the callback to + /// get back the original managed object the handle refers to. + /// + /// Since the handle is allocated the GC will keep it alive until freed. Thus, + /// the callback can use the object it gets. Normally, + /// is called from a final callback method but it is also possible that the API gurantess + /// only a single call to a provided method where it would release the native context + /// after materializing it to a managed form. (After materialization, normal GC rules + /// apply to the managed instance such that while it is in scope it is not released by + /// the GC) + /// + /// + public static class NativeContext + { + /// Extension method to get a native consumable context pointer for a managed object + /// Type of the managed object + /// Managed object to get the context for + /// Native API consumable "pointer" for a GC handle + /// + /// This creates a from the instance and gets a native ABI + /// pointer from that. The returned value is a native pointer that is convertible + /// back to a handle, which can then allow access to the original . + /// + /// Normally the materialization of a managed target is done via + /// in a callback implementation. Ultimately the allocated handle is freed in a call to . + /// + /// + public static unsafe void* AsNativeContext(this T self) + { + var handle = GCHandle.Alloc( self ); + try + { + return GCHandle.ToIntPtr(handle).ToPointer(); + } + catch + { + handle.Free(); + throw; + } + } + + /// Materializes an instance from a native ABI context as a void* + /// Type of the managed object to materialize + /// Native context + /// Materialized value or if not + /// if the value is succesfully materialized and if not + public static unsafe bool TryFrom(void* ctx, [MaybeNullWhen(false)] out T value) + { + if(ctx is not null && GCHandle.FromIntPtr( (nint)ctx ).Target is T managedInst) + { + value = managedInst; + return true; + } + + value = default; + return false; + } + + /// Releases a native context handle + /// Reference to the context to release + /// + /// This releases the native context AND sets to + /// so that no subsequent releases are not possible (or get an exception). + /// + [SuppressMessage( "Design", "CA1045:Do not pass types by reference", Justification = "Allows controlled reset to null (invalid)" )] + public static unsafe void Release(ref void* ctx) + { + Release(ctx); + ctx = null; + } + + /// Releases the context without resetting it to + /// Context to release + /// + /// This does NOT re-assign the context as it is intended for use from within a call back + /// that performs the release. In such a case any field or property of the containing type + /// should be set to to prevent a double free problem. Use of this + /// method is discouraged as it requires more care, but is sometimes required. + /// + public static unsafe void Release(void* ctx) + { + Debug.Assert(ctx is not null, "Attempting to release a NULL context - something is likely wrong in the caller!"); + + // SAFETY - NOP if null, don't trigger a crash in native code. + // Debugger assert above will trigger in debug builds. + if(ctx is not null) + { + GCHandle.FromIntPtr((nint)ctx) + .Free(); + } + } + } +} diff --git a/src/Ubiquity.NET.InteropHelpers/Readme.md b/src/Ubiquity.NET.InteropHelpers/Readme.md index 11fed4774..5adfffb7b 100644 --- a/src/Ubiquity.NET.InteropHelpers/Readme.md +++ b/src/Ubiquity.NET.InteropHelpers/Readme.md @@ -1,8 +1,8 @@ # About -Ubiquity.NET.InteropHelpers helper support common to low level interop libraries. While this -library is intended to support the Ubiquity.NET.Llvm interop requirements there isn't anything -bound to that library in the support here. That is it is independent and a useful library for -any code base providing interop support. +Ubiquity.NET.InteropHelpers helper support common to low level interop libraries. While +this library is intended to support the Ubiquity.NET.Llvm interop requirements there isn't +anything bound to that library in the support here. That is it is independent and a useful +library for any code base providing interop support. # Key Features * String handling @@ -18,10 +18,10 @@ any code base providing interop support. * Delegates and NativeCallbacks as Function pointers * Function pointers are a new feature of C# that makes for very high performance interop scenarios. However, sometimes the callback for a function pointer actually needs - additional data not part of the parameters of the function to work properly. This library - provides support for such scenarios where a delegate is used to "capture" the data while - still supporting AOT scenarios. (NOTE: Marshal.GetFunctionPointerForDelegate() must - dynamically emit a thunk that contains the proper signature and the captured "this" - pointer so is NOT AOT friendly) The support offered in this library, though a bit more - tedious, is AOT friendly. + additional data not part of the parameters of the function to work properly. This + library provides support for such scenarios where a delegate is used to "capture" the + data while still supporting AOT scenarios. (NOTE: Marshal.GetFunctionPointerForDelegate() + must dynamically emit a thunk that contains the proper signature and the captured + "this" pointer so is NOT AOT friendly) The support offered in this library, though a + bit more tedious, is AOT friendly. diff --git a/src/Ubiquity.NET.InteropHelpers/SafeGCHandle.cs b/src/Ubiquity.NET.InteropHelpers/SafeGCHandle.cs deleted file mode 100644 index 038b40882..000000000 --- a/src/Ubiquity.NET.InteropHelpers/SafeGCHandle.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Ubiquity.NET Contributors. All rights reserved. -// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. - -using System.Runtime.InteropServices; - -namespace Ubiquity.NET.InteropHelpers -{ - /// Helper type to wrap GC handles with ref counting - /// - /// Instances of this class (as a private member of a controlled type) help - /// to ensure the referenced object stays alive while in use for native ABI as a - /// "context" for callbacks. This is the key to ref counted behavior to hold 'o' - /// (and anything it references) alive for the GC. The "ownership" of the refcount - /// is handed to native code while the calling code is free to no longer reference - /// the containing instance as it holds an allocated GCHandle for itself and THAT - /// is kept alive by a ref count that is "owned" by native code. - /// - /// This is used as a member of such a holding type so that 'AddRefAndGetNativeContext' - /// retrieves a marshaled GCHandle for the containing/Controlling instance that is - /// then provided as the native "Context" parameter (usually as a void*). - /// - /// It is assumed that instances of this type are held in a disposable type such - /// that when the containing type is disposed, then this is disposed. Additionally, - /// this assumes that the native code MIGHT dispose of this instance and that callers - /// should otherwise account for the ref count increase to hold the instance alive. That - /// is, by holding a to self, with an AddRef'd handle the instance - /// would live until the app is terminated! Thus applications using this MUST understand - /// the native code use and account for the disposable of any instances with this as a - /// member. Typically a callback provided to the native code is used to indicate release - /// of the resource. That callback will call dispose to decrement the refcount on the - /// handle. If the ref count lands at 0, then the object it refers to is subject to - /// normal GC. - /// - public sealed class SafeGCHandle - : SafeHandle - { - /// Initializes a new instance of the class. - /// Object to allocate a GCHandle for that is controlled by this instance - /// - /// It is expected that the type of has this - /// as a private member so that it controls the lifetime of it's container. - /// - public SafeGCHandle( object o ) - : base( 0, ownsHandle: true ) - { - handle = (nint)GCHandle.Alloc( o ); - } - - /// - public override bool IsInvalid => handle == 0; - - /// Adds a ref count to this handle AND converts the allocated for use in native code - /// context value for use in native code to refer to the held by this instance - /// - /// A native call back that receives the returned context can reconstitute the via - /// and from that it can get the original instance the handle refers to via - /// - public unsafe nint AddRefAndGetNativeContext( ) - { - bool ignoredButRequired = false; - DangerousAddRef( ref ignoredButRequired ); - return handle; - } - - /// - protected override bool ReleaseHandle( ) - { - GCHandle.FromIntPtr( handle ).Free(); - return true; - } - } -} diff --git a/src/Ubiquity.NET.Llvm.JIT.Tests/OrcJitTests.cs b/src/Ubiquity.NET.Llvm.JIT.Tests/OrcJitTests.cs index dd926fe61..09f63cabf 100644 --- a/src/Ubiquity.NET.Llvm.JIT.Tests/OrcJitTests.cs +++ b/src/Ubiquity.NET.Llvm.JIT.Tests/OrcJitTests.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/src/Ubiquity.NET.Llvm/Context.cs b/src/Ubiquity.NET.Llvm/Context.cs index d85dd8b4f..48044f774 100644 --- a/src/Ubiquity.NET.Llvm/Context.cs +++ b/src/Ubiquity.NET.Llvm/Context.cs @@ -263,6 +263,17 @@ public void Dispose( ) { if(Handle != nint.Zero) { + // remove any diagnostic handler so it the callback's context doesn't leak + unsafe + { + void* context = LLVMContextGetDiagnosticContext(Handle); + if(context is not null) + { + LLVMContextSetDiagnosticHandler(Handle, null, null); + NativeContext.Release(context); + } + } + Handle.Dispose(); InvalidateAfterMove(); } diff --git a/src/Ubiquity.NET.Llvm/ContextAlias.cs b/src/Ubiquity.NET.Llvm/ContextAlias.cs index 775e4149e..48a4c22ca 100644 --- a/src/Ubiquity.NET.Llvm/ContextAlias.cs +++ b/src/Ubiquity.NET.Llvm/ContextAlias.cs @@ -144,10 +144,19 @@ public AttributeValue CreateAttribute( LazyEncodedString name, LazyEncodedString public void SetDiagnosticHandler( DiagnosticInfoCallbackAction handler ) { - using var callBack = new DiagnosticCallbackHolder(handler); unsafe { - LLVMContextSetDiagnosticHandler( Handle, &DiagnosticCallbackHolder.DiagnosticHandler, callBack.AddRefAndGetNativeContext() ); + void* ctx = null; + try + { + ctx = handler.AsNativeContext(); + LLVMContextSetDiagnosticHandler( Handle, &DiagnosticCallbacks.DiagnosticHandler, ctx ); + } + catch when (ctx is not null) + { + NativeContext.Release(ctx); + throw; + } } } diff --git a/src/Ubiquity.NET.Llvm/DiagnosticCallbackHolder.cs b/src/Ubiquity.NET.Llvm/DiagnosticCallbacks.cs similarity index 59% rename from src/Ubiquity.NET.Llvm/DiagnosticCallbackHolder.cs rename to src/Ubiquity.NET.Llvm/DiagnosticCallbacks.cs index d796297e3..81d4b9cf4 100644 --- a/src/Ubiquity.NET.Llvm/DiagnosticCallbackHolder.cs +++ b/src/Ubiquity.NET.Llvm/DiagnosticCallbacks.cs @@ -18,54 +18,31 @@ namespace Ubiquity.NET.Llvm /// public delegate void DiagnosticInfoCallbackAction( DiagnosticInfo info ); - internal sealed class DiagnosticCallbackHolder - : IDisposable + // native callbacks for an LLVM context Diagnostic messages. + internal static class DiagnosticCallbacks { - public DiagnosticCallbackHolder( DiagnosticInfoCallbackAction diagnosticHandler ) - { - Delegate = diagnosticHandler; - AllocatedSelf = new( this ); - } - - public void Dispose( ) - { - if(!AllocatedSelf.IsInvalid && !AllocatedSelf.IsClosed) - { - // Decrements the ref count on the handle - // might not actually destroy anything - AllocatedSelf.Dispose(); - } - } - - internal unsafe nint AddRefAndGetNativeContext( ) - { - return AllocatedSelf.AddRefAndGetNativeContext(); - } - [UnmanagedCallersOnly( CallConvs = [ typeof( CallConvCdecl ) ] )] [SuppressMessage( "Design", "CA1031:Do not catch general exception types", Justification = "REQUIRED for unmanaged callback - Managed exceptions must never cross the boundary to native code" )] internal static unsafe void DiagnosticHandler( nint abiInfo, void* context ) { try { - if(MarshalGCHandle.TryGet( context, out DiagnosticCallbackHolder? self )) + if(NativeContext.TryFrom(context, out var self )) { - self.Delegate( new( abiInfo ) ); + self( new( abiInfo ) ); } } catch { - // stop in debugger as this is a detected app error. + // SAFETY: stop in debugger as this is a detected app error. // Test for attached debugger directly to avoid prompts, WER cruft etc... - // End user should NOT be prompted to attach a debugger! + // End user should NOT be prompted to attach a debugger, if one is already + // attached then stop as the resulting exception is likely an app crash! if(Debugger.IsAttached) { Debugger.Break(); } } } - - private readonly DiagnosticInfoCallbackAction Delegate; - private readonly SafeGCHandle AllocatedSelf; } } diff --git a/src/Ubiquity.NET.Llvm/DisAssembler.cs b/src/Ubiquity.NET.Llvm/DisAssembler.cs index 407e0f7e5..da5234656 100644 --- a/src/Ubiquity.NET.Llvm/DisAssembler.cs +++ b/src/Ubiquity.NET.Llvm/DisAssembler.cs @@ -1,9 +1,6 @@ // Copyright (c) Ubiquity.NET Contributors. All rights reserved. // Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. -// this file declares and uses the "experimental" interface `IDisassemblerCallbacks`. -#pragma warning disable LLVM002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - using static Ubiquity.NET.Llvm.Interop.ABI.llvm_c.Disassembler; namespace Ubiquity.NET.Llvm @@ -18,8 +15,8 @@ namespace Ubiquity.NET.Llvm /// but also applies to the /// `TagBuf` parameter of /// - [SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Closely related only used here" )] - [Experimental( "LLVM002" )] + [SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Closely related; only used here" )] + [Experimental( "LLVMEXP003" )] public interface IDisassemblerCallbacks { /// Purpose not fully known or well explained in LLVM docs @@ -69,11 +66,22 @@ public enum DisassemblerOptions } /// LLVM Disassembler + [Experimental( "LLVMEXP003" )] public sealed class Disassembler : IDisposable { /// - public void Dispose( ) => Handle.Dispose(); + public void Dispose( ) + { + Handle.Dispose(); + unsafe + { + if(CallbacksHandle is not null) + { + NativeContext.Release(ref CallbacksHandle); + } + } + } /// Initializes a new instance of the class. /// Triple for the instruction set to disassemble @@ -121,16 +129,24 @@ public Disassembler( Triple triple unsafe { ArgumentNullException.ThrowIfNull( triple ); - CallBacksHandle = callBacks is null ? null : GCHandle.Alloc( callBacks ); - Handle = LLVMCreateDisasmCPUFeatures( - triple.ToString() ?? LazyEncodedString.Empty, - cpu, - features, - CallBacksHandle.HasValue ? GCHandle.ToIntPtr( CallBacksHandle.Value ).ToPointer() : null, - tagType, - CallBacksHandle.HasValue ? &NativeInfoCallBack : null, - CallBacksHandle.HasValue ? &NativeSymbolLookupCallback : null - ); + CallbacksHandle = callBacks is null ? null : callBacks.AsNativeContext(); + try + { + Handle = LLVMCreateDisasmCPUFeatures( + triple.ToString() ?? LazyEncodedString.Empty, + cpu, + features, + CallbacksHandle, // The context provided to callbacks + tagType, + CallbacksHandle is null ? null : &NativeInfoCallBack, + CallbacksHandle is null ? null : &NativeSymbolLookupCallback + ); + } + catch when (CallbacksHandle is not null) + { + NativeContext.Release(ref CallbacksHandle); + throw; + } } } @@ -167,7 +183,7 @@ public bool SetOptions( DisassemblerOptions options ) } } - private readonly GCHandle? CallBacksHandle; + private unsafe void* CallbacksHandle; private readonly LLVMDisasmContextRef Handle; #region Native marshalling callbacks @@ -184,7 +200,7 @@ public bool SetOptions( DisassemblerOptions options ) { try { - if(!MarshalGCHandle.TryGet( disInfo, out IDisassemblerCallbacks? callBacks )) + if(!NativeContext.TryFrom(disInfo, out var callBacks )) { return null; } @@ -223,7 +239,7 @@ private static unsafe int NativeInfoCallBack( void* disInfo, UInt64 pc, UInt64 o { try { - return MarshalGCHandle.TryGet( disInfo, out IDisassemblerCallbacks? callBacks ) + return NativeContext.TryFrom(disInfo, out var callBacks ) ? callBacks.OpInfo( pc, offset, opSize, instSize, tagType, (nint)tagBuf ) : 0; // TODO: Is this a legit failure return value? } diff --git a/src/Ubiquity.NET.Llvm/GlobalNamespaceImports.cs b/src/Ubiquity.NET.Llvm/GlobalNamespaceImports.cs index 69593005b..ff5ec5c6a 100644 --- a/src/Ubiquity.NET.Llvm/GlobalNamespaceImports.cs +++ b/src/Ubiquity.NET.Llvm/GlobalNamespaceImports.cs @@ -20,6 +20,7 @@ where it belongs. global using System.Buffers; global using System.Collections; global using System.Collections.Generic; +global using System.Collections.Immutable; global using System.Collections.ObjectModel; global using System.Diagnostics; global using System.Diagnostics.CodeAnalysis; diff --git a/src/Ubiquity.NET.Llvm/ILibLLVM.cs b/src/Ubiquity.NET.Llvm/ILibLLVM.cs index 38f8a6c46..129f31a95 100644 --- a/src/Ubiquity.NET.Llvm/ILibLLVM.cs +++ b/src/Ubiquity.NET.Llvm/ILibLLVM.cs @@ -2,8 +2,6 @@ // Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. // This is cribbed from the interop library to prevent the need for applications to take a direct dependency on the interop library -using System.Collections.Immutable; - namespace Ubiquity.NET.Llvm { /// Code gen target to register/initialize diff --git a/src/Ubiquity.NET.Llvm/IOperandCollection.cs b/src/Ubiquity.NET.Llvm/IOperandCollection.cs index c657756bc..cf2de5c3c 100644 --- a/src/Ubiquity.NET.Llvm/IOperandCollection.cs +++ b/src/Ubiquity.NET.Llvm/IOperandCollection.cs @@ -29,7 +29,6 @@ public interface IOperandCollection /// Inclusive start index for the slice /// Exclusive end index for the slice /// Slice of the collection - [SuppressMessage( "Naming", "CA1716:Identifiers should not match keywords", Justification = "Naming is consistent with System.Range parameters" )] IOperandCollection Slice( int start, int end ) { return new OperandCollectionSlice( this, new Range( start, end ) ); diff --git a/src/Ubiquity.NET.Llvm/Instructions/InstructionExtensions.cs b/src/Ubiquity.NET.Llvm/Instructions/InstructionExtensions.cs index a4bdaa210..161b0792f 100644 --- a/src/Ubiquity.NET.Llvm/Instructions/InstructionExtensions.cs +++ b/src/Ubiquity.NET.Llvm/Instructions/InstructionExtensions.cs @@ -3,6 +3,16 @@ namespace Ubiquity.NET.Llvm.Instructions { + // This does NOT use the new C# 14 extension syntax due to several reasons + // 1) Code lens does not work https://github.com/dotnet/roslyn/issues/79006 [Sadly marked as "not planned" - e.g., dead-end] + // 2) MANY analyzers get things wrong and need to be supressed (CA1000, CA1034, and many others [SAxxxx]) + // 3) Many tools (like docfx don't support the new syntax yet) + // 4) No clear support for Caller* attributes ([CallerArgumentExpression(...)]). + // + // Bottom line it's a good idea with an incomplete implementation lacking support + // in the overall ecosystem. Don't use it unless you absolutely have to until all + // of that is sorted out. + /// Provides extension methods to that cannot be achieved as members of the class /// /// Using generic static extension methods allows for fluent coding while retaining the type of the "this" parameter. diff --git a/src/Ubiquity.NET.Llvm/Library.cs b/src/Ubiquity.NET.Llvm/Library.cs index 80f2a90c4..2ed8905f9 100644 --- a/src/Ubiquity.NET.Llvm/Library.cs +++ b/src/Ubiquity.NET.Llvm/Library.cs @@ -7,8 +7,6 @@ of this library from direct dependencies on the interop library. If a consumer h the low level interop (Test code sometimes does) it must explicitly reference it. */ -using System.Collections.Immutable; - using static Ubiquity.NET.Llvm.Interop.ABI.libllvm_c.AttributeBindings; using static Ubiquity.NET.Llvm.Interop.ABI.llvm_c.DebugInfo; diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/CustomMaterializationUnit.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/CustomMaterializationUnit.cs index d42ae0212..039a9d2f0 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/CustomMaterializationUnit.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/CustomMaterializationUnit.cs @@ -1,8 +1,6 @@ // Copyright (c) Ubiquity.NET Contributors. All rights reserved. // Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. -using System.Collections.Immutable; - using static Ubiquity.NET.Llvm.Interop.ABI.llvm_c.Orc; namespace Ubiquity.NET.Llvm.OrcJITv2 @@ -10,8 +8,8 @@ namespace Ubiquity.NET.Llvm.OrcJITv2 /// Delegate to perform action on Materialization /// that serves as the context for this materialization /// - /// This must be a "custom" delegate as the is a - /// ref type that is NOT allowed as a type parameter for . + /// This is "custom" delegate for consistency with + /// which requires one for .NET runtimes lower than .NET 9. /// public delegate void MaterializationAction( MaterializationResponsibility r ); @@ -22,7 +20,7 @@ namespace Ubiquity.NET.Llvm.OrcJITv2 /// This must be a "custom" delegate as the is a /// ref type that is NOT allowed as a type parameter for . /// - public delegate void DiscardAction( JITDyLib jitLib, SymbolStringPoolEntry symbol ); + public delegate void DiscardAction( ref readonly JITDyLib jitLib, SymbolStringPoolEntry symbol ); /// LLVM ORC JIT v2 custom materialization unit /// @@ -91,48 +89,47 @@ private static LLVMOrcMaterializationUnitRef MakeHandle( ValidateInitSym(initSymbol, symbols); LLVMOrcMaterializationUnitRef retVal; - // This will internally manage the lifetime -#pragma warning disable IDE0063 // Use simple 'using' statement - using( var materializer = new CustomMaterializer(materializeAction, discardAction)) + using IMemoryOwner nativeSyms = symbols.InitializeNativeCopy(); + + // using expression ensures cleanup of addref in case of exceptions... + // But since it is an immutable Value type that isn't viable here and try/finally is used + LLVMOrcSymbolStringPoolEntryRef nativeInitSym = initSymbol?.AddRefForNative() ?? default; + unsafe { - using(IMemoryOwner nativeSyms = symbols.InitializeNativeCopy()) + void* nativeContext = null; // init only from inside try/catch block + try { - nint nativeContext = materializer.AddRefAndGetNativeContext(); - - // using expression ensures cleanup of addref in case of exceptions... - // But since it is an immutable Value type that isn't viable here and try/finally is used - LLVMOrcSymbolStringPoolEntryRef nativeInitSym = initSymbol?.AddRefForNative() ?? default; - try - { - unsafe - { - using var pinnedSyms = nativeSyms.Memory.Pin(); - retVal = LLVMOrcCreateCustomMaterializationUnit( - name, - (void*)nativeContext, - (LLVMOrcCSymbolFlagsMapPair*)pinnedSyms.Pointer, - checked((nuint)symbols.Length), - nativeInitSym, - &NativeCallbacks.Materialize, - materializer.SupportsDiscard ? &NativeCallbacks.Discard : null, - &NativeCallbacks.Destroy - ); - - // ownership of this symbol was moved to native, mark transfer so auto clean up (for exceptional cases) - // does NOT kick in. - nativeInitSym = default; - } - } - finally + nativeContext = new MaterializerCallbacksHolder(materializeAction, discardAction).AsNativeContext(); + using var pinnedSyms = nativeSyms.Memory.Pin(); + retVal = LLVMOrcCreateCustomMaterializationUnit( + name, + nativeContext, + (LLVMOrcCSymbolFlagsMapPair*)pinnedSyms.Pointer, + checked((nuint)symbols.Length), + nativeInitSym, + &NativeCallbacks.Materialize, + &NativeCallbacks.Discard, + &NativeCallbacks.Destroy + ); + + // ownership of this symbol was moved to native, mark transfer so auto clean up (for exceptional cases) + // does NOT kick in. + nativeInitSym = default; + } + catch when (nativeContext is not null) + { + // release the handle allocated for the native code as it isn't used there in the face of an exception here + NativeContext.Release( ref nativeContext); + throw; + } + finally + { + if(!nativeInitSym.IsNull) { - if(!nativeInitSym.IsNull) - { - nativeInitSym.Dispose(); - } + nativeInitSym.Dispose(); } } } -#pragma warning restore IDE0063 // Use simple 'using' statement return retVal; } @@ -159,22 +156,18 @@ internal static unsafe void Materialize( void* context, /*LLVMOrcMaterialization { try { - if(MarshalGCHandle.TryGet( context, out CustomMaterializer? self )) + if(NativeContext.TryFrom(context, out var self )) { + // Destroy callback is NOT called if this one is... + // Internally LLVM will set the context to null [Undocumented!] + NativeContext.Release(context); + #pragma warning disable CA2000 // Dispose objects before losing scope #pragma warning disable IDISP004 // Don't ignore created IDisposable // [It is an alias; Dispose is a NOP with wasted overhead] - self.MaterializeHandler( new MaterializationResponsibility( abiResponsibility, alias: true ) ); + self.Materialize( new MaterializationResponsibility( abiResponsibility, alias: true ) ); #pragma warning restore IDISP004 // Don't ignore created IDisposable #pragma warning restore CA2000 // Dispose objects before losing scope - -#pragma warning disable IDISP007 // Don't dispose injected - /* - Not really "injected" and this is how the data/context is disposed when the - native code is done with it. - */ - self.Dispose(); -#pragma warning restore IDISP007 // Don't dispose injected } } catch @@ -189,12 +182,13 @@ internal static unsafe void Discard( void* context, /*LLVMOrcJITDylibRef*/ nint { try { - if(MarshalGCHandle.TryGet( context, out CustomMaterializer? self )) + if(NativeContext.TryFrom( context, out var self )) { #pragma warning disable CA2000 // Dispose objects before losing scope #pragma warning disable IDISP004 // Don't ignore created IDisposable // [It is an alias; Dispose is a NOP with wasted overhead] - self.DiscardHandler?.Invoke( new JITDyLib( abiLib ), new SymbolStringPoolEntry( abiSymbol, alias: true ) ); + var managedLib = new JITDyLib( abiLib ); + self.Discard( in managedLib, new SymbolStringPoolEntry( abiSymbol, alias: true ) ); #pragma warning restore IDISP004 // Don't ignore created IDisposable #pragma warning restore CA2000 // Dispose objects before losing scope } @@ -211,15 +205,13 @@ internal static unsafe void Destroy( void* context ) { try { - if(MarshalGCHandle.TryGet( context, out CustomMaterializer? self )) + if(NativeContext.TryFrom(context, out var self )) { -#pragma warning disable IDISP007 // Don't dispose injected - /* - Not really "injected" and this is how the data/context is disposed when the - native code is done with it. - */ - self.Dispose(); -#pragma warning restore IDISP007 // Don't dispose injected + // self is a managed instance with normal GC rules now so release + // the context created for callbacks as it is not needed anymore. + // After this scope exits, GC is free to collect the instance. + NativeContext.Release(context); + self.Destroy(); } } catch diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/CustomMaterializer.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/CustomMaterializer.cs deleted file mode 100644 index 4866ad212..000000000 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/CustomMaterializer.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Ubiquity.NET Contributors. All rights reserved. -// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. - -namespace Ubiquity.NET.Llvm.OrcJITv2 -{ - /// Holds delegates for performing custom materialization for a single materialization unit - internal sealed class CustomMaterializer - : IDisposable - { - /// Initializes a new instance of the class. - /// Action to perform to materialize the symbol - /// Action to perform when the JIT discards/replaces a symbol - public CustomMaterializer( MaterializationAction materializeAction, DiscardAction? discardAction ) - { - AllocatedSelf = new( this ); - MaterializeHandler = materializeAction; - DiscardHandler = discardAction; - } - - /// - public void Dispose( ) - { - if(!AllocatedSelf.IsInvalid && !AllocatedSelf.IsClosed) - { - // Decrements the ref count on the handle - // might not actually destroy anything - AllocatedSelf.Dispose(); - } - } - - internal bool SupportsDiscard => DiscardHandler is not null; - - internal unsafe nint AddRefAndGetNativeContext( ) - { - return AllocatedSelf.AddRefAndGetNativeContext(); - } - - internal MaterializationAction MaterializeHandler { get; init; } - - internal DiscardAction? DiscardHandler { get; init; } - - // This is the key to ref counted behavior to hold this instance (and anything it references) - // alive for the GC. The "ownership" of the refcount is handed to native code while the - // calling code is free to no longer reference this instance as it holds an allocated - // GCHandle for itself and THAT is kept alive by a ref count that is "owned" by native code. - private SafeGCHandle AllocatedSelf { get; init; } - } -} diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/ExecutionSession.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/ExecutionSession.cs index ba10381f2..9e5aa3e1e 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/ExecutionSession.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/ExecutionSession.cs @@ -54,7 +54,7 @@ out LLVMOrcLazyCallThroughManagerRef resultHandle } /* - [Experimental] + [Experimental("????")] public void Lookup(LookupKind kind, scoped ReadOnlySpan order, scoped ReadOnlySpan symbols, LookupResultHandler handler) { // validate args... @@ -264,6 +264,7 @@ internal ExecutionSession( nint h ) internal LLVMOrcExecutionSessionRef Handle { get; init; } +#if FUTURE_DEVELOPMENT_AREA /// Native code callback for error reporting /// The context for the callback is a with a target of /// ABI handle for an error ref @@ -292,5 +293,6 @@ private static unsafe void NativeErrorReporterCallback( void* context, /*LLVMErr Debug.Assert( false, "Exception in native callback!" ); } } +#endif } } diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/GlobalMemoryAllocatorBase.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/GlobalMemoryAllocatorBase.cs new file mode 100644 index 000000000..f886ac9f2 --- /dev/null +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/GlobalMemoryAllocatorBase.cs @@ -0,0 +1,70 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. +using static Ubiquity.NET.Llvm.Interop.ABI.llvm_c.OrcEE; + +namespace Ubiquity.NET.Llvm.OrcJITv2 +{ + /// Base class for a Global allocator for use as an Object layer in OrcJITv2 + /// + /// Instances of this are provided as an object layer via an implementation of . + /// While this type implements via it is ONLY intended for + /// clean up on exceptions. A factory returns the instances to the native JIT, which takes over ownership. + /// + /// Derived types need not be concrend with any of the low level native interop. Instead + /// they simply implement the abstract methods to perform the required allocations. This + /// base type handles all of the interop, including the reverse P/Invoke marhsalling for + /// callbacks. + /// + /// + [Experimental("LLVMEXP004")] + public abstract class GlobalMemoryAllocatorBase + : ObjectLayer + , IJitMemoryAllocator + { + /// Initializes a new instance of the class. + /// Session for this object layer + protected GlobalMemoryAllocatorBase(ExecutionSession session) + : base() + { + unsafe + { + CallbackContext = this.AsNativeContext(); + Handle = LLVMOrcCreateRTDyldObjectLinkingLayerWithMCJITMemoryManagerLikeCallbacks( + session.Handle, + CallbackContext, + &MemoryAllocatorNativeCallbacks.CreatePerObjContextAsGlobalContext, + &MemoryAllocatorNativeCallbacks.NotifyTerminating, // Releases the global context + &MemoryAllocatorNativeCallbacks.AllocateCodeSection, + &MemoryAllocatorNativeCallbacks.AllocateDataSection, + &MemoryAllocatorNativeCallbacks.FinalizeMemory, + &MemoryAllocatorNativeCallbacks.DestroyPerObjContextNOP // NOP + ); + } + } + + /// + public abstract nuint AllocateCodeSection(nuint size, UInt32 alignment, UInt32 sectionId, LazyEncodedString sectionName ); + + /// + public abstract nuint AllocateDataSection(nuint size, UInt32 alignment, UInt32 sectionId, LazyEncodedString sectionName, bool isReadOnly); + + /// + public abstract bool FinalizeMemory([NotNullWhen(false)] out LazyEncodedString? errMsg); + + /// + public virtual void ReleaseContext() + { + unsafe + { + NativeContext.Release(ref CallbackContext); + } + } + + //protected override void Dispose( bool disposing ) + //{ + // Do NOT overload this, the base will dispose the object layer ReleaseContext() is called to release the call back context + //} + + private unsafe void* CallbackContext; + } +} diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/IJitMemoryAllocator.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/IJitMemoryAllocator.cs new file mode 100644 index 000000000..a935d48ea --- /dev/null +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/IJitMemoryAllocator.cs @@ -0,0 +1,63 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. + +#pragma warning disable SA1649 // Filename must match type name +#pragma warning disable SA1600 // Elements must be documented +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 'Type_or_Member' + +namespace Ubiquity.NET.Llvm.OrcJITv2 +{ + public interface IJitMemoryAllocator + { + /// Allocate a block of contiguous memory for use as code execution by the native code JIT engine + /// Size of the block + /// alignment requirements of the block + /// ID for the section + /// Name of the section + /// Address of the first byte of the allocated memory + /// + /// If the memory is allocated from the managed heap then the returned address MUST + /// remain pinned until is called on this allocator + /// + /// The Execute only page setting and any other page properties is not applied to the returned + /// address (or entire memory of the allocated section) until is called. + /// This allows the JIT to write code into the memory area even if it is ultimately Execute-Only. + /// + /// + nuint AllocateCodeSection(nuint size, UInt32 alignment, UInt32 sectionId, LazyEncodedString sectionName ); + + /// Allocate a block of contiguous memory for use as data by the native code JIT engine + /// Size of the block + /// alignment requirements of the block + /// ID for the section + /// Name of the section + /// Memory section is Read-Only + /// Address of the first byte of the allocated memory + /// + /// If the memory is allocated from the managed heap then the returned address MUST + /// remain pinned until is called on this allocator. + /// + /// The and any other page properties is not applied to the returned + /// address (or entire memory of the allocated section) until is called. + /// This allows the JIT to write initial data into the memory even if it is ultimately Read-Only. + /// + /// + nuint AllocateDataSection(nuint size, UInt32 alignment, UInt32 sectionId, LazyEncodedString sectionName, bool isReadOnly); + + /// Finalizes a previous allocation by applying page settings for the allocation + /// Error message in the event of a failure + /// if successfull ( is ); if not ( has the reason) + bool FinalizeMemory([NotNullWhen(false)] out LazyEncodedString? errMsg); + + /// Release the context for the memory. No further callbacks will occur for this allocator + /// + /// This is similar to a call to except that it releases only the native context + /// in respnse to a callback, not the handle for allocator itself. That MUST live longer than the JIT as any memory + /// it allocated MAY still be in use as code or data in the JIT. (This interface only deals with WHOLE JIT memory + /// allocation. It is at least plausible to have an allocator per JitDyLib but that would end up needing to leverage + /// a global one to ensure that secion ordering and size limits of the underlying OS are met. If such a things is + /// ever implented, it would use a different interface for clarity.) + /// + void ReleaseContext(); + } +} diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/IMaterializerCallbacks.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/IMaterializerCallbacks.cs new file mode 100644 index 000000000..d11c72caa --- /dev/null +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/IMaterializerCallbacks.cs @@ -0,0 +1,21 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. + +namespace Ubiquity.NET.Llvm.OrcJITv2 +{ + /// Interface for a type that receives call backs for materialization (usually a type derived from ) + public interface IMaterializerCallbacks + { + /// Materializes all symbols in this unit except those that were previously discarded + /// that serves as the context for this materialization + void Materialize( MaterializationResponsibility r ); + + /// Discards a symbol overwridden by the JIT (Before materialization) + /// Library the symbols is discarded from + /// Symbol being discarded + void Discard( ref readonly JITDyLib jitLib, SymbolStringPoolEntry symbol ); + + /// Destroys the materializer callback context + public void Destroy(); + } +} diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/IrTransformLayer.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/IrTransformLayer.cs index 6df80b4a6..dbf7dd2cd 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/IrTransformLayer.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/IrTransformLayer.cs @@ -38,13 +38,21 @@ public void Emit( MaterializationResponsibility r, ThreadSafeModule tsm ) /// Action to perform that transforms modules materialized in a JIT public void SetTransform( TransformAction transformAction ) { - // Create a holder for the action; the Dispose will take care of things - // in the event of an exception and will become a NOP if transfer of - // ownership completes. - using var holder = new TransformCallback(transformAction); unsafe { - LLVMOrcIRTransformLayerSetTransform( Handle, TransformCallback.Callback, (void*)holder.AddRefAndGetNativeContext() ); + // Create a holder for the action; the try/catch will take care of things + // in the event of an exception and will be a NOP if transfer of ownership + // completes. + void* ctx = transformAction.AsNativeContext(); + try + { + LLVMOrcIRTransformLayerSetTransform( Handle, &TransformCallback.Transform, ctx ); + } + catch when (ctx is not null) + { + NativeContext.Release(ref ctx); + throw; + } } } @@ -57,33 +65,10 @@ internal IrTransformLayer( LLVMOrcIRTransformLayerRef h ) // internal keep alive holder for a native call back as a delegate private sealed class TransformCallback - : IDisposable { - public TransformCallback( TransformAction transformAction ) - { - AllocatedSelf = new( this ); - TransformAction = transformAction; - } - - public void Dispose( ) - { - AllocatedSelf.Dispose(); - } - - internal TransformAction TransformAction { get; } - - internal unsafe nint AddRefAndGetNativeContext( ) - { - return AllocatedSelf.AddRefAndGetNativeContext(); - } - - private readonly SafeGCHandle AllocatedSelf; - - internal static unsafe delegate* unmanaged[Cdecl]< void*, nint*, nint, nint > Callback => &Transform; - [UnmanagedCallersOnly( CallConvs = [ typeof( CallConvCdecl ) ] )] [SuppressMessage( "Design", "CA1031:Do not catch general exception types", Justification = "REQUIRED for unmanaged callback - Managed exceptions must never cross the boundary to native code" )] - private static unsafe /*LLVMErrorRef*/ nint Transform( + internal static unsafe /*LLVMErrorRef*/ nint Transform( void* context, /*LLVMOrcThreadSafeModuleRef* */nint* modInOut, /*LLVMOrcMaterializationResponsibilityRef*/ nint resp @@ -98,7 +83,7 @@ internal unsafe nint AddRefAndGetNativeContext( ) try { - if(!MarshalGCHandle.TryGet( context, out TransformCallback? self )) + if(!NativeContext.TryFrom(context, out var self )) { return LLVMErrorRef.CreateForNativeOut( "Internal Error: Invalid context provided for native callback"u8 ); } @@ -112,7 +97,7 @@ internal unsafe nint AddRefAndGetNativeContext( ) // if replaceMode is not null then it is moved to the native caller as an "out" param // Dispose, even if NOP, is just wasted overhead. - self.TransformAction( tsm, responsibility, out ThreadSafeModule? replacedMod ); + self( tsm, responsibility, out ThreadSafeModule? replacedMod ); #pragma warning restore CA2000 // Dispose objects before losing scope #pragma warning restore IDISP001 // Dispose created if(replacedMod is not null) diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/LLJitBuilder.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/LLJitBuilder.cs index 61dcc7f12..c3055f6fd 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/LLJitBuilder.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/LLJitBuilder.cs @@ -30,9 +30,22 @@ public LLJitBuilder( TargetMachineBuilder builder ) public void Dispose( ) { // ensure move semantics work by making this Idempotent. + // If handle is null this instance does not own it (or the context) if(!Handle.IsNull) { Handle.Dispose(); + unsafe + { + if(ObjectLinkingLayerContext is not null) + { + // If the callback context handle exists and is allocated then free it now. + // The handle for this builder itself is already closed so LLVM should not + // have any callbacks for this to handle... (docs are silent on the point) + NativeContext.Release(ObjectLinkingLayerContext); + ObjectLinkingLayerContext = null; + } + } + InvalidateAfterMove(); } } @@ -57,14 +70,14 @@ public void SetTargetMachineBuilder( TargetMachineBuilder targetMachineBuilder ) public void SetObjectLinkingLayerCreator( ObjectLayerFactory creator ) { ArgumentNullException.ThrowIfNull( creator ); - ObjectLinkingLayerContextHandle = new( creator ); - unsafe { + ObjectLinkingLayerContext = creator.AsNativeContext(); + LLVMOrcLLJITBuilderSetObjectLinkingLayerCreator( Handle, &NativeObjectLinkingLayerCreatorCallback, - (void*)ObjectLinkingLayerContextHandle.AddRefAndGetNativeContext() + ObjectLinkingLayerContext ); } } @@ -124,27 +137,14 @@ public static LLJitBuilder CreateBuilderForHost( private void InvalidateAfterMove( ) { Handle = default; - - // If the callback context handle exists and is allocated then free it now. - // The handle for this builder itself is already closed so LLVM should not - // have any callbacks for this to handle... (docs are silent on the point) - if(!ObjectLinkingLayerContextHandle.IsInvalid && !ObjectLinkingLayerContextHandle.IsClosed) - { - ObjectLinkingLayerContextHandle.Dispose(); - } - - // Break any GC references to allow release - InternalObjectLayerFactory = null; } private LLJitBuilder( LLVMOrcLLJITBuilderRef h ) { Handle = h; - ObjectLinkingLayerContextHandle = new( this ); } - private SafeGCHandle ObjectLinkingLayerContextHandle; - private ObjectLayerFactory? InternalObjectLayerFactory; + private unsafe void* ObjectLinkingLayerContext = null; [UnmanagedCallersOnly( CallConvs = [ typeof( CallConvCdecl ) ] )] [SuppressMessage( "Design", "CA1031:Do not catch general exception types", Justification = "REQUIRED for unmanaged callback - Managed exceptions must never cross the boundary to native code" )] @@ -156,16 +156,17 @@ private LLJitBuilder( LLVMOrcLLJITBuilderRef h ) { try { - if(MarshalGCHandle.TryGet( context, out LLJitBuilder? self ) && self.InternalObjectLayerFactory is not null) + if(NativeContext.TryFrom(context, out var self )) { using var managedTriple = new Triple(LazyEncodedString.FromUnmanaged(triple)); // caller takes ownership of the resulting handle; Don't Dispose it - var factory = self.InternalObjectLayerFactory( new ExecutionSession( sessionRef ), managedTriple ); + // It is returned via the raw native handle. + var factory = self( new ExecutionSession( sessionRef ), managedTriple ); return factory.Handle; } - // TODO: How/Can this report an error? Internally the result is (via a C++ lambda) that returns an "llvm::expected" + // TODO: How/Can this report an error? Internally the result is (via a C++ lambda) assigned to an "llvm::expected" // but it is unclear if this method can return an error or what happens on a null return... return 0; // This will probably crash in LLVM anyway - best effort. } diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/MaterializerCallbacksHolder.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/MaterializerCallbacksHolder.cs new file mode 100644 index 000000000..d8fe21647 --- /dev/null +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/MaterializerCallbacksHolder.cs @@ -0,0 +1,44 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. +namespace Ubiquity.NET.Llvm.OrcJITv2 +{ + /// Holds delegates for performing custom materialization for a single materialization unit + /// + /// Instances of this type serve as the native context for callbacks. This is an internal holder of delegates + /// instead of an interface to allow for inline functions and lambdas etc.. for each action. This is not + /// possible if only an interface is used. + /// + internal sealed class MaterializerCallbacksHolder + : IMaterializerCallbacks + { + /// Initializes a new instance of the class. + /// Action to perform to materialize the symbol + /// Action to perform when the JIT discards/replaces a symbol + public MaterializerCallbacksHolder( MaterializationAction materializeAction, DiscardAction? discardAction ) + { + MaterializeHandler = materializeAction; + DiscardHandler = discardAction; + } + + public void Destroy( ) + { + } + + public void Discard( ref readonly JITDyLib jitLib, SymbolStringPoolEntry symbol ) + { + if(DiscardHandler is not null) + { + DiscardHandler(in jitLib, symbol); + } + } + + public void Materialize( MaterializationResponsibility r ) + { + MaterializeHandler(r); + } + + internal MaterializationAction MaterializeHandler { get; private set; } + + internal DiscardAction? DiscardHandler { get; private set; } + } +} diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/MemoryAllocatorNativeCallbacks.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/MemoryAllocatorNativeCallbacks.cs new file mode 100644 index 000000000..7daa85323 --- /dev/null +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/MemoryAllocatorNativeCallbacks.cs @@ -0,0 +1,105 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. + +namespace Ubiquity.NET.Llvm.OrcJITv2 +{ + // Native only callbacks that use a GCHandle converted to a void* as the + // "context" to allow the native API to re-direct to the proper managed + // implementation instance. (That is, this is a reverse P/Invoke that + // handles marshalling of parameters and return type for native callers + // into managed code) + + internal static class MemoryAllocatorNativeCallbacks + { + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe void *CreatePerObjContextAsGlobalContext(void* outerContext) + { + // Provide the "global"/"outer" context as the "inner"/"Per OBJ" context + return outerContext; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe void DestroyPerObjContextNOP(void *_) + { + /* Intentional NOP */ + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe byte* AllocateCodeSection(void* ctx, nuint size, UInt32 alignment, UInt32 sectionId, byte* sectionName) + { +#pragma warning disable CA2000 // Dispose objects before losing scope + // NOT dispsable, just "borrowed" via ctx + return NativeContext.TryFrom( ctx, out var self ) + ? (byte*)self!.AllocateCodeSection(size, alignment, sectionId, LazyEncodedString.FromUnmanaged(sectionName)) + : (byte*)null; +#pragma warning restore CA2000 // Dispose objects before losing scope + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe byte* AllocateDataSection( + void* ctx, + nuint size, + UInt32 alignment, + UInt32 sectionId, + byte* sectionName, + /*LLVMBool*/Int32 isReadOnly + ) + { +#pragma warning disable CA2000 // Dispose objects before losing scope + // NOT dispsable, just "borrowed" via ctx + return NativeContext.TryFrom( ctx, out var self ) + ? (byte*)self!.AllocateDataSection(size, alignment, sectionId, LazyEncodedString.FromUnmanaged(sectionName), isReadOnly != 0) + : (byte*)null; +#pragma warning restore CA2000 // Dispose objects before losing scope + + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe /*LLVMStatus*/ Int32 FinalizeMemory(void* ctx, byte** errMsg) + { + *errMsg = null; +#pragma warning disable CA2000 // Dispose objects before losing scope + // NOT dispsable, just "borrowed" via ctx + if(NativeContext.TryFrom( ctx, out var self )) + { + if(!self!.FinalizeMemory(out LazyEncodedString? managedErrMsg)) + { + AllocateAndSetNativeMessage( errMsg, managedErrMsg.ToReadOnlySpan(includeTerminator: true)); + } + } +#pragma warning restore CA2000 // Dispose objects before losing scope + + AllocateAndSetNativeMessage( errMsg, "Invalid context provided to FinalizeMemory callback!"u8); + return 0; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe void NotifyTerminating(void* ctx) + { +#pragma warning disable CA2000 // Dispose objects before losing scope + // NOT dispsable, just "borrowed" via ctx + if(NativeContext.TryFrom( ctx, out var self )) + { + // Don't dispose it here; But do release the context + // as no more callbacks will occur after this. + // Dispose controls the allocator handle itself, NOT the + // context used for the callbacks. This ONLY releases the + // callback context. + self.ReleaseContext(); + } +#pragma warning restore CA2000 // Dispose objects before losing scope + } + + // WARNING: Native caller ***WILL*** call `free(*errMsg)` if `*errMsg != nullptr`!! [Undocumented!] + // Therefore, any error message returned should be allocated with NativeMemory.Alloc() + // to allow free() on the pointer. + private static unsafe void AllocateAndSetNativeMessage( byte** errMsg, ReadOnlySpan managedErrMsg ) + { + nuint len = (nuint)managedErrMsg.Length; + void* p = NativeMemory.Alloc(len); + var nativeMsgSpan = new Span(p, (int)len); + managedErrMsg.CopyTo( nativeMsgSpan ); + *errMsg = (byte*)p; + } + } +} diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/ObjectLayer.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/ObjectLayer.cs index 04ab2a25b..60a0ff630 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/ObjectLayer.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/ObjectLayer.cs @@ -6,8 +6,14 @@ namespace Ubiquity.NET.Llvm.OrcJITv2 { /// ORC JIT v2 Object linking layer - public sealed class ObjectLayer - : IDisposable + /// + /// Since instances of an Object Linking layer are ONLY ever created by and + /// returned directly to the native code as the raw handle, they are not generally disposable. They do + /// implement IDisposable as a means to enusre proper release in the face of an exception in an implementation + /// of but are not something that generally needs disposal at a managed level. + /// + public class ObjectLayer + : DisposableObject { /// Adds an object file to the specified library /// Library to add the object file to @@ -59,9 +65,10 @@ public void Emit( MaterializationResponsibility resp, MemoryBuffer objBuffer ) } /// - [SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP007:Don't dispose injected", Justification = "Ownership transferred in constructor")] - public void Dispose( ) + [SuppressMessage( "IDisposableAnalyzers.Correctness", "IDISP007:Don't dispose injected", Justification = "Ownership transferred in constructor" )] + protected override void Dispose( bool disposing ) { + base.Dispose( disposing ); if(!Handle.IsNull) { Handle.Dispose(); @@ -74,6 +81,25 @@ internal ObjectLayer( LLVMOrcObjectLayerRef h ) Handle = h; } - internal LLVMOrcObjectLayerRef Handle { get; private set; } + internal ObjectLayer() + { + } + + internal LLVMOrcObjectLayerRef Handle + { + get; + + // Only accessible from derived types, since the modifier of this property is `internal` that + // means `internal` AND `protected` + private protected set + { + if(!field.IsNull) + { + throw new InvalidOperationException("INTERNAL: Setting handle multiple times is not allowed!"); + } + + field = value; + } + } } } diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/SimpleJitMemoryAllocatorBase.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/SimpleJitMemoryAllocatorBase.cs new file mode 100644 index 000000000..25b834b89 --- /dev/null +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/SimpleJitMemoryAllocatorBase.cs @@ -0,0 +1,93 @@ +// Copyright (c) Ubiquity.NET Contributors. All rights reserved. +// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. + +#pragma warning disable SA1649 // Filename must match type name +#pragma warning disable SA1600 // Elements must be documented +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 'Type_or_Member' + +using static Ubiquity.NET.Llvm.Interop.ABI.llvm_c.ExecutionEngine; + +namespace Ubiquity.NET.Llvm.OrcJITv2 +{ + /// Base class for a simple MCJIT style memory allocator + /// + /// Derived types need not be concrend with any of the low level native interop. Instead + /// they simply implement the abstract methods to perform the required allocations. This + /// base type handles all of the interop, including the reverse P/Invoke marhsalling for + /// callbacks. + /// + [Experimental("LLVMEXP005")] + public abstract class SimpleJitMemoryAllocatorBase + : DisposableObject + , IJitMemoryAllocator + { + protected SimpleJitMemoryAllocatorBase() + { + unsafe + { + CallbackContext = this.AsNativeContext(); + + Handle = LLVMCreateSimpleMCJITMemoryManager( + CallbackContext, + &MemoryAllocatorNativeCallbacks.AllocateCodeSection, + &MemoryAllocatorNativeCallbacks.AllocateDataSection, + &MemoryAllocatorNativeCallbacks.FinalizeMemory, + &MemoryAllocatorNativeCallbacks.NotifyTerminating + ); + } + } + + /// + public abstract nuint AllocateCodeSection(nuint size, UInt32 alignment, UInt32 sectionId, LazyEncodedString sectionName ); + + /// + public abstract nuint AllocateDataSection(nuint size, UInt32 alignment, UInt32 sectionId, LazyEncodedString sectionName, bool isReadOnly); + + /// + public abstract bool FinalizeMemory([NotNullWhen(false)] out LazyEncodedString? errMsg); + + /// + public virtual void ReleaseContext() + { + unsafe + { + NativeContext.Release(ref CallbackContext); + } + } + + /// + protected override void Dispose( bool disposing ) + { + base.Dispose( disposing ); + if(disposing) + { + if(!Handle.IsNull) + { + Handle.Dispose(); + Handle = default; + } + + // Releases the allocated handle for the native code + // might not actually destroy anything. This is INTENTIONALLY done + // AFTER disposing the handle so that any pending call backs + // use a valid context. After the memory manager handle is destroyed + // no more call backs should occur so it is safe to release the + // context. (Ideally, the ReleaseContext() callback already happened + // but the LLVM docs, and code comments, are silent on the point. Thus, + // that uses a ref parameter that is set to null and a null check is + // applied here.) + unsafe + { + if(CallbackContext is not null) + { + NativeContext.Release(ref CallbackContext); + } + } + } + } + + private unsafe void* CallbackContext; + + private LLVMMCJITMemoryManagerRef Handle; + } +} diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/SymbolStringPool.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/SymbolStringPool.cs index 35f3da0aa..a1eb0f9df 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/SymbolStringPool.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/SymbolStringPool.cs @@ -1,8 +1,6 @@ // Copyright (c) Ubiquity.NET Contributors. All rights reserved. // Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information. -using System.Collections.Immutable; - using static Ubiquity.NET.Llvm.Interop.ABI.libllvm_c.OrcJITv2Bindings; using static Ubiquity.NET.Llvm.Interop.ABI.llvm_c.Orc; diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/SymbolStringPoolEntry.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/SymbolStringPoolEntry.cs index b739422c4..0a066bc13 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/SymbolStringPoolEntry.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/SymbolStringPoolEntry.cs @@ -207,7 +207,7 @@ internal SymbolStringPoolEntry( LLVMOrcSymbolStringPoolEntryRef h, bool addRef = /// Initializes a new instance of the class. /// Native ABI handle /// Indicates whether this is an aliased (unowned) representation - /// Wraps a native ABI handle in a managed projected type + /// Wraps a native ABI handle for an entry in a string pool in a managed type internal SymbolStringPoolEntry( nint abiHandle, bool alias = false ) { Handle = LLVMOrcSymbolStringPoolEntryRef.FromABI(abiHandle); diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/ThreadSafeModule.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/ThreadSafeModule.cs index b035bf5d2..9e4c78964 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/ThreadSafeModule.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/ThreadSafeModule.cs @@ -119,14 +119,14 @@ private static LLVMOrcThreadSafeModuleRef MakeHandle( ThreadSafeContext context, { try { - if(!MarshalGCHandle.TryGet( context, out GenericModuleOperation? self )) + if(!NativeContext.TryFrom(context, out var self )) { return LLVMErrorRef.CreateForNativeOut( "Internal Error: Invalid context provided for native callback"u8 ); } IModule mod = new ModuleAlias(moduleHandle); return mod is not null - ? self( mod ).MoveToNative() + ? self!( mod ).MoveToNative() : LLVMErrorRef.CreateForNativeOut( "Internal Error: Could not create wrapped module for native method"u8 ); } catch(Exception ex) diff --git a/src/Ubiquity.NET.Llvm/Types/IPointerType.cs b/src/Ubiquity.NET.Llvm/Types/IPointerType.cs index aefceb968..49e167639 100644 --- a/src/Ubiquity.NET.Llvm/Types/IPointerType.cs +++ b/src/Ubiquity.NET.Llvm/Types/IPointerType.cs @@ -24,6 +24,16 @@ public interface IPointerType ITypeRef? ElementType { get; init; } } + // This does NOT use the new C# 14 extension syntax due to several reasons + // 1) Code lens does not work https://github.com/dotnet/roslyn/issues/79006 [Sadly marked as "not planned" - e.g., dead-end] + // 2) MANY analyzers get things wrong and need to be supressed (CA1000, CA1034, and many others [SAxxxx]) + // 3) Many tools (like docfx don't support the new syntax yet) + // 4) No clear support for Caller* attributes ([CallerArgumentExpression(...)]). + // + // Bottom line it's a good idea with an incomplete implementation lacking support + // in the overall ecosystem. Don't use it unless you absolutely have to until all + // of that is sorted out. + /// Utility class to provide extensions for /// /// These are useful even in the presence of default property implementations as the properties @@ -43,12 +53,6 @@ public static class PointerTypeExtensions /// but is not normal for anything that creates or clones the IR. Since the pointer type creation is /// done as a method of the thing being pointed to this information is "attached" to the pointer so /// that the is not . - /// - /// until C# 14 [.Net 10] is supported this is an extension method. Once C#14 is available - /// then this can become a property. Default methods on the interface have too many restrictions - /// (most egregious is the need to "box"/cast to the explicit interface for the lookup to find - /// the method). - /// /// public static bool IsOpaque( this IPointerType ptr ) { diff --git a/src/Ubiquity.NET.TextUX/AssemblyExtensions.cs b/src/Ubiquity.NET.TextUX/AssemblyExtensions.cs index 384e2540d..15191612e 100644 --- a/src/Ubiquity.NET.TextUX/AssemblyExtensions.cs +++ b/src/Ubiquity.NET.TextUX/AssemblyExtensions.cs @@ -6,33 +6,20 @@ namespace Ubiquity.NET.TextUX { + // This does NOT use the new C# 14 extension syntax due to several reasons + // 1) Code lens does not work https://github.com/dotnet/roslyn/issues/79006 [Sadly marked as "not planned" - e.g., dead-end] + // 2) MANY analyzers get things wrong and need to be supressed (CA1000, CA1034, and many others [SAxxxx]) + // 3) Many tools (like docfx don't support the new syntax yet) + // 4) No clear support for Caller* attributes ([CallerArgumentExpression(...)]). + // + // Bottom line it's a good idea with an incomplete implementation lacking support + // in the overall ecosystem. Don't use it unless you absolutely have to until all + // of that is sorted out. + /// Utility class to provide extensions for consumers [SuppressMessage( "Design", "CA1034:Nested types should not be visible", Justification = "BS, extension" )] public static class AssemblyExtensions { -// Sadly support for the 'extension' keyword outside of the compiler is -// spotty at best. Third party tools and analyzers don't know what to do -// with it. First party analyzers and tools don't yet handle it properly. -// (Looking at you VS 2026 Insider's preview!) So don't use it yet... -#if ALL_TOOLS_SUPPORT_EXTENSION_KEYWORD - /// Extensions for - extension(Assembly asm) - { - /// Gets the value of the from an assembly - [SuppressMessage( "Performance", "CA1822:Mark members as static", Justification = "BS, extension" )] - public string InformationalVersion - { - get - { - var assemblyVersionAttribute = asm.GetCustomAttribute(); - - return assemblyVersionAttribute is not null - ? assemblyVersionAttribute.InformationalVersion - : asm.GetName().Version?.ToString() ?? string.Empty; - } - } - } -#else /// Gets the value of the from an assembly /// Assembly to get informational version from /// Information version of the assembly or an empty string if not available @@ -45,6 +32,5 @@ public static string GetInformationalVersion(this Assembly self) ? assemblyVersionAttribute.InformationalVersion : self.GetName().Version?.ToString() ?? string.Empty; } -#endif } } diff --git a/src/Ubiquity.NET.TextUX/PackageReadMe.md b/src/Ubiquity.NET.TextUX/PackageReadMe.md index e4e5d768d..4abdde830 100644 --- a/src/Ubiquity.NET.TextUX/PackageReadMe.md +++ b/src/Ubiquity.NET.TextUX/PackageReadMe.md @@ -6,7 +6,9 @@ Text based UI/UX. This is generally only relevant for console based apps. `IDiagnosticReporter` interface is at the core of the UX. It is similar in many ways to many of the logging interfaces available. The primary distinction is with the ***intention*** of use. `IDiagnosticReporter` specifically assumes the use for UI/UX rather than a -debugging/diagnostic log. +debugging/diagnostic log. These have VERY distinct use cases and purposes and generally show +very different information. (Not to mention the overlly complex requirements of +the anti-pattern DI container assumed in `Microsoft.Extensions.Logging`) ### Messages All messages for the UX use a simple immutable structure to store the details of a message @@ -17,7 +19,8 @@ There are a few pre-built implementation of the `IDiagnosticReporter` interface. * `TextWriterReporter` * Base class for writing UX to a `TextWriter` * `ConsoleReporter` - * Reporter that reports errors to `Console.Error` and all other errors to `Console.Out` + * Reporter that reports errors to `Console.Error` and all other nessages to + `Console.Out` * `ColoredConsoleReporter` * `ConsoleReporter` that colorizes output using ANSI color codes * Colors are customizable, but contains a common default diff --git a/src/Ubiquity.NET.TextUX/SourceRange.cs b/src/Ubiquity.NET.TextUX/SourceRange.cs index e2ac00fd7..19e842147 100644 --- a/src/Ubiquity.NET.TextUX/SourceRange.cs +++ b/src/Ubiquity.NET.TextUX/SourceRange.cs @@ -11,6 +11,11 @@ namespace Ubiquity.NET.TextUX // Column position of the location [0..n-1] /// Abstraction to hold a source location range as a pair of values + /// + /// It is possible that some sources do not provide dual points to make a proper range. This supports such + /// "ranges" as a single point that is not sliceable. This allows callers to deal with sources that only + /// contain the start point (Looking at you !). + /// public readonly record struct SourceRange : IFormattable {