From ba13a5de7baca426525afbc96b119e87a6b5474b Mon Sep 17 00:00:00 2001 From: Steven Maillet Date: Mon, 20 Oct 2025 12:50:56 -0700 Subject: [PATCH 1/2] Add Custom memory manager support to JIT * This adds the types but doesn't yet use/test them - Future PR will add them once testing is done * Fixed declaration of native callback function pointers - Can't use bool for LLVMBool as that requires marshaling * Added debug assert for `SafeGCHandle.AddRefAndGetNativeContext` to complain in a debug build if the underlying AddRef fails for some reason. - Docs on behavior of that API are a bit dodgy and it isn't clear how it could ever provide an out parameter of non-success * Added additional preprocessor checks to disable unused code * Added `IJitMemoryAllocator` to support either MCJIT simple allocator or an OrcJitV2 Object layer. * Readme and doc comment corrections/clarifications --- .../ABI/llvm-c/ExecutionEngine.cs | 4 +- .../ABI/llvm-c/OrcEE.cs | 4 +- .../SafeGCHandle.cs | 8 +- .../OrcJITv2/ExecutionSession.cs | 2 + .../OrcJITv2/IJitMemoryAllocator.cs | 53 ++++++++++ .../MemoryAllocatorNativeCallbacks.cs | 87 +++++++++++++++ src/Ubiquity.NET.Llvm/OrcJITv2/ObjectLayer.cs | 28 ++++- .../OrcJITv2/RTDyldObjMemoryAllocatorBase.cs | 99 +++++++++++++++++ .../OrcJITv2/SimpleJitMemoryAllocatorBase.cs | 100 ++++++++++++++++++ src/Ubiquity.NET.TextUX/PackageReadMe.md | 7 +- src/Ubiquity.NET.TextUX/SourceRange.cs | 5 + 11 files changed, 384 insertions(+), 13 deletions(-) create mode 100644 src/Ubiquity.NET.Llvm/OrcJITv2/IJitMemoryAllocator.cs create mode 100644 src/Ubiquity.NET.Llvm/OrcJITv2/MemoryAllocatorNativeCallbacks.cs create mode 100644 src/Ubiquity.NET.Llvm/OrcJITv2/RTDyldObjMemoryAllocatorBase.cs create mode 100644 src/Ubiquity.NET.Llvm/OrcJITv2/SimpleJitMemoryAllocatorBase.cs 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/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.InteropHelpers/SafeGCHandle.cs b/src/Ubiquity.NET.InteropHelpers/SafeGCHandle.cs index 038b40882..e2ebcf9d7 100644 --- a/src/Ubiquity.NET.InteropHelpers/SafeGCHandle.cs +++ b/src/Ubiquity.NET.InteropHelpers/SafeGCHandle.cs @@ -1,6 +1,7 @@ // 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.Runtime.InteropServices; namespace Ubiquity.NET.InteropHelpers @@ -25,10 +26,10 @@ namespace Ubiquity.NET.InteropHelpers /// 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 + /// the native code use and account for the disposale 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 + /// of the resource. That callback will call dispose to decrement the refcount on this + /// GC handle. If the ref count lands at 0, then the object it refers to is subject to /// normal GC. /// public sealed class SafeGCHandle @@ -59,6 +60,7 @@ public unsafe nint AddRefAndGetNativeContext( ) { bool ignoredButRequired = false; DangerousAddRef( ref ignoredButRequired ); + Debug.Assert(ignoredButRequired, "DangerousAddRef on the GC handle did NOT succeed!"); return handle; } diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/ExecutionSession.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/ExecutionSession.cs index ba10381f2..ecce613bb 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/ExecutionSession.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/ExecutionSession.cs @@ -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/IJitMemoryAllocator.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/IJitMemoryAllocator.cs new file mode 100644 index 000000000..59b77c8b0 --- /dev/null +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/IJitMemoryAllocator.cs @@ -0,0 +1,53 @@ +// 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 + : IDisposable + { + /// 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); + } +} diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/MemoryAllocatorNativeCallbacks.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/MemoryAllocatorNativeCallbacks.cs new file mode 100644 index 000000000..dbb6cf316 --- /dev/null +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/MemoryAllocatorNativeCallbacks.cs @@ -0,0 +1,87 @@ +// 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 SafeGCHandle as the "context" to allow + // the native API to re-direct to the proper managed implementation instance. + // (That is, this is a revers 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 *CreateContext(void* outerContext) + { + return outerContext; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe void DestroyContext(void *ctx) + { + /* Intentional NOP */ + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe byte* AllocateCodeSection(void* ctx, nuint size, UInt32 alignment, UInt32 sectionId, byte* sectionName) + { + return MarshalGCHandle.TryGet( ctx, out IJitMemoryAllocator? self ) + ? (byte*)self.AllocateCodeSection(size, alignment, sectionId, LazyEncodedString.FromUnmanaged(sectionName)) + : (byte*)null; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe byte* AllocateDataSection( + void* ctx, + nuint size, + UInt32 alignment, + UInt32 sectionId, + byte* sectionName, + /*LLVMBool*/Int32 isReadOnly + ) + { + return MarshalGCHandle.TryGet( ctx, out IJitMemoryAllocator? self ) + ? (byte*)self.AllocateDataSection(size, alignment, sectionId, LazyEncodedString.FromUnmanaged(sectionName), isReadOnly != 0) + : (byte*)null; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe /*LLVMStatus*/ Int32 FinalizeMemory(void* ctx, byte** errMsg) + { + *errMsg = null; + if(MarshalGCHandle.TryGet( ctx, out IJitMemoryAllocator? self )) + { + if(!self.FinalizeMemory(out LazyEncodedString? managedErrMsg)) + { + AllocateAndSetNativeMessage( errMsg, managedErrMsg.ToReadOnlySpan(includeTerminator: true)); + } + } + + AllocateAndSetNativeMessage( errMsg, "Invalid context provided to FinalizeMemory callback!"u8); + return 0; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe void DestroyMemory(void* ctx) + { + if(MarshalGCHandle.TryGet( ctx, out IJitMemoryAllocator? self )) + { +#pragma warning disable IDISP007 // Don't dispose injected + // NOT injected; ref counted, native code is releasing ref count via this call + self.Dispose(); +#pragma warning restore IDISP007 // Don't dispose injected + } + } + + // WARNING: Native caller ***WILL*** call `free(*errMsg)` if `*errMsg != nullptr`!! + // 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..4fde63224 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/ObjectLayer.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/ObjectLayer.cs @@ -6,8 +6,8 @@ namespace Ubiquity.NET.Llvm.OrcJITv2 { /// ORC JIT v2 Object linking layer - public sealed class ObjectLayer - : IDisposable + public class ObjectLayer + : DisposableObject { /// Adds an object file to the specified library /// Library to add the object file to @@ -60,8 +60,9 @@ public void Emit( MaterializationResponsibility resp, MemoryBuffer objBuffer ) /// [SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP007:Don't dispose injected", Justification = "Ownership transferred in constructor")] - public void Dispose( ) + protected override void Dispose( bool disposing ) { + base.Dispose(disposing); if(!Handle.IsNull) { Handle.Dispose(); @@ -74,6 +75,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/RTDyldObjMemoryAllocatorBase.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/RTDyldObjMemoryAllocatorBase.cs new file mode 100644 index 000000000..b886c08ff --- /dev/null +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/RTDyldObjMemoryAllocatorBase.cs @@ -0,0 +1,99 @@ +// 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 an allocator for use as an Object layer in OrcJITv2 + public abstract class RTDyldObjMemoryAllocatorBase + : ObjectLayer + , IJitMemoryAllocator + { + /// Initializes a new instance of the class. + /// Session for this object layer + protected RTDyldObjMemoryAllocatorBase(ExecutionSession session) + : base() + { + AllocatedSelf = new(this); + unsafe + { + Handle = LLVMOrcCreateRTDyldObjectLinkingLayerWithMCJITMemoryManagerLikeCallbacks( + session.Handle, + (void*)AddRefAndGetNativeContext(), + &MemoryAllocatorNativeCallbacks.CreateContext, + &MemoryAllocatorNativeCallbacks.DestroyMemory, // Notify Terminating + &MemoryAllocatorNativeCallbacks.AllocateCodeSection, + &MemoryAllocatorNativeCallbacks.AllocateDataSection, + &MemoryAllocatorNativeCallbacks.FinalizeMemory, + &MemoryAllocatorNativeCallbacks.DestroyContext // NOP + ); + } + } + + /// 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. + /// + /// + public abstract 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. + /// + /// + public abstract 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) + public abstract bool FinalizeMemory([NotNullWhen(false)] out LazyEncodedString? errMsg); + + /// + protected override void Dispose( bool disposing ) + { + if(disposing && !AllocatedSelf.IsInvalid && !AllocatedSelf.IsClosed) + { + // Decrements the ref count on the handle + // might not actually destroy anything + AllocatedSelf.Dispose(); + } + + base.Dispose( disposing ); + } + + internal unsafe nint AddRefAndGetNativeContext( ) + { + ObjectDisposedException.ThrowIf(IsDisposed, this); + + return AllocatedSelf.AddRefAndGetNativeContext(); + } + + // 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; } + } +} diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/SimpleJitMemoryAllocatorBase.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/SimpleJitMemoryAllocatorBase.cs new file mode 100644 index 000000000..3d8c5afd0 --- /dev/null +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/SimpleJitMemoryAllocatorBase.cs @@ -0,0 +1,100 @@ +// 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 +{ + public abstract class SimpleJitMemoryAllocatorBase + : DisposableObject + , IJitMemoryAllocator + { + protected SimpleJitMemoryAllocatorBase() + { + AllocatedSelf = new(this); + unsafe + { + Handle = LLVMCreateSimpleMCJITMemoryManager( + (void*)AddRefAndGetNativeContext(), + &MemoryAllocatorNativeCallbacks.AllocateCodeSection, + &MemoryAllocatorNativeCallbacks.AllocateDataSection, + &MemoryAllocatorNativeCallbacks.FinalizeMemory, + &MemoryAllocatorNativeCallbacks.DestroyMemory + ); + } + } + + /// 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. + /// + /// + public abstract 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. + /// + /// + public abstract 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) + public abstract bool FinalizeMemory([NotNullWhen(false)] out LazyEncodedString? errMsg); + + /// + protected override void Dispose( bool disposing ) + { + if(disposing && !AllocatedSelf.IsInvalid && !AllocatedSelf.IsClosed) + { + // Decrements the ref count on the handle + // might not actually destroy anything + AllocatedSelf.Dispose(); + } + + Handle = default; + base.Dispose( disposing ); + } + + internal unsafe nint AddRefAndGetNativeContext( ) + { + ObjectDisposedException.ThrowIf(IsDisposed, this); + + return AllocatedSelf.AddRefAndGetNativeContext(); + } + + // 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; } + + private LLVMMCJITMemoryManagerRef Handle; + } +} 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 { From ce9db228081b54c1339aadda3a653892f5068270 Mon Sep 17 00:00:00 2001 From: Steven Maillet Date: Thu, 23 Oct 2025 15:52:54 -0700 Subject: [PATCH 2/2] Updates for simpler native contexts Native contexts are effectively an opaque `void*` they can have any value that fits in a pointer. The native layers don't care about it and store the value to provide as a parameter to callback functions. The implementation of the callback has all the knowledge to interpret the intended meaning of the context value. For managed code this is an allocated GCHandle converted to a pointer. The managed code can resolve the pointer to a handle, and then the target instance to get a viable managed ref. When no more call backs can occur the context is released. This simplifies the use of callbacks as a native context can be created for any managed heap object. (Extension method added for this action) * Updated comments * Removed some dead code * Added comments to explain limits of the C# 14 keyword `extension` and why it isn't used. * Removed SafeGCHandle and MarshalGCHandles as context handles are MUCH simpler now. * Added the memory allocator base types - These are not yet used or tested and are subject to change in the future and therfore attributed as experimental. * Defined interface for materializer callbacks to allow internal implementation to hold delegates as a single unit to become a native context for call backs. * Removed unnecessary using statements --- .../ABI/StringMarshaling/LLVMErrorRef.cs | 25 ++-- .../ABI/llvm-c/Core.cs | 4 +- .../ABI/llvm-c/Orc.cs | 4 +- .../RawApiTests.cs | 1 - .../ParseResultExtensions.cs | 45 ++----- .../FluentValidationExtensions.cs | 22 +++- .../MarshalGCHandle.cs | 46 ------- .../NativeContext.cs | 119 ++++++++++++++++++ src/Ubiquity.NET.InteropHelpers/Readme.md | 20 +-- .../SafeGCHandle.cs | 74 ----------- .../OrcJitTests.cs | 1 - src/Ubiquity.NET.Llvm/Context.cs | 11 ++ src/Ubiquity.NET.Llvm/ContextAlias.cs | 13 +- ...llbackHolder.cs => DiagnosticCallbacks.cs} | 37 ++---- src/Ubiquity.NET.Llvm/DisAssembler.cs | 54 +++++--- .../GlobalNamespaceImports.cs | 1 + src/Ubiquity.NET.Llvm/ILibLLVM.cs | 2 - src/Ubiquity.NET.Llvm/IOperandCollection.cs | 1 - .../Instructions/InstructionExtensions.cs | 10 ++ src/Ubiquity.NET.Llvm/Library.cs | 2 - .../OrcJITv2/CustomMaterializationUnit.cs | 114 ++++++++--------- .../OrcJITv2/CustomMaterializer.cs | 48 ------- .../OrcJITv2/ExecutionSession.cs | 2 +- .../OrcJITv2/GlobalMemoryAllocatorBase.cs | 70 +++++++++++ .../OrcJITv2/IJitMemoryAllocator.cs | 12 +- .../OrcJITv2/IMaterializerCallbacks.cs | 21 ++++ .../OrcJITv2/IrTransformLayer.cs | 47 +++---- .../OrcJITv2/LLJitBuilder.cs | 41 +++--- .../OrcJITv2/MaterializerCallbacksHolder.cs | 44 +++++++ .../MemoryAllocatorNativeCallbacks.cs | 56 ++++++--- src/Ubiquity.NET.Llvm/OrcJITv2/ObjectLayer.cs | 10 +- .../OrcJITv2/RTDyldObjMemoryAllocatorBase.cs | 99 --------------- .../OrcJITv2/SimpleJitMemoryAllocatorBase.cs | 99 +++++++-------- .../OrcJITv2/SymbolStringPool.cs | 2 - .../OrcJITv2/SymbolStringPoolEntry.cs | 2 +- .../OrcJITv2/ThreadSafeModule.cs | 4 +- src/Ubiquity.NET.Llvm/Types/IPointerType.cs | 16 ++- src/Ubiquity.NET.TextUX/AssemblyExtensions.cs | 34 ++--- 38 files changed, 595 insertions(+), 618 deletions(-) delete mode 100644 src/Ubiquity.NET.InteropHelpers/MarshalGCHandle.cs create mode 100644 src/Ubiquity.NET.InteropHelpers/NativeContext.cs delete mode 100644 src/Ubiquity.NET.InteropHelpers/SafeGCHandle.cs rename src/Ubiquity.NET.Llvm/{DiagnosticCallbackHolder.cs => DiagnosticCallbacks.cs} (59%) delete mode 100644 src/Ubiquity.NET.Llvm/OrcJITv2/CustomMaterializer.cs create mode 100644 src/Ubiquity.NET.Llvm/OrcJITv2/GlobalMemoryAllocatorBase.cs create mode 100644 src/Ubiquity.NET.Llvm/OrcJITv2/IMaterializerCallbacks.cs create mode 100644 src/Ubiquity.NET.Llvm/OrcJITv2/MaterializerCallbacksHolder.cs delete mode 100644 src/Ubiquity.NET.Llvm/OrcJITv2/RTDyldObjMemoryAllocatorBase.cs 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/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/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 e2ebcf9d7..000000000 --- a/src/Ubiquity.NET.InteropHelpers/SafeGCHandle.cs +++ /dev/null @@ -1,74 +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; -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 disposale 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 this - /// GC 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 ); - Debug.Assert(ignoredButRequired, "DangerousAddRef on the GC handle did NOT succeed!"); - 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 ecce613bb..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... 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 index 59b77c8b0..a935d48ea 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/IJitMemoryAllocator.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/IJitMemoryAllocator.cs @@ -8,7 +8,6 @@ namespace Ubiquity.NET.Llvm.OrcJITv2 { public interface IJitMemoryAllocator - : IDisposable { /// Allocate a block of contiguous memory for use as code execution by the native code JIT engine /// Size of the block @@ -49,5 +48,16 @@ public interface IJitMemoryAllocator /// 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 index dbb6cf316..7daa85323 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/MemoryAllocatorNativeCallbacks.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/MemoryAllocatorNativeCallbacks.cs @@ -3,20 +3,23 @@ namespace Ubiquity.NET.Llvm.OrcJITv2 { - // Native only callbacks that use a SafeGCHandle as the "context" to allow - // the native API to re-direct to the proper managed implementation instance. - // (That is, this is a revers P/Invoke that handles marshalling of parameters - // and return type for native callers into managed code) + // 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 *CreateContext(void* outerContext) + 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 DestroyContext(void *ctx) + internal static unsafe void DestroyPerObjContextNOP(void *_) { /* Intentional NOP */ } @@ -24,9 +27,12 @@ internal static unsafe void DestroyContext(void *ctx) [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] internal static unsafe byte* AllocateCodeSection(void* ctx, nuint size, UInt32 alignment, UInt32 sectionId, byte* sectionName) { - return MarshalGCHandle.TryGet( ctx, out IJitMemoryAllocator? self ) - ? (byte*)self.AllocateCodeSection(size, alignment, sectionId, LazyEncodedString.FromUnmanaged(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)])] @@ -39,40 +45,52 @@ internal static unsafe void DestroyContext(void *ctx) /*LLVMBool*/Int32 isReadOnly ) { - return MarshalGCHandle.TryGet( ctx, out IJitMemoryAllocator? self ) - ? (byte*)self.AllocateDataSection(size, alignment, sectionId, LazyEncodedString.FromUnmanaged(sectionName), isReadOnly != 0) +#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; - if(MarshalGCHandle.TryGet( ctx, out IJitMemoryAllocator? self )) +#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)) + 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 DestroyMemory(void* ctx) + internal static unsafe void NotifyTerminating(void* ctx) { - if(MarshalGCHandle.TryGet( ctx, out IJitMemoryAllocator? self )) +#pragma warning disable CA2000 // Dispose objects before losing scope + // NOT dispsable, just "borrowed" via ctx + if(NativeContext.TryFrom( ctx, out var self )) { -#pragma warning disable IDISP007 // Don't dispose injected - // NOT injected; ref counted, native code is releasing ref count via this call - self.Dispose(); -#pragma warning restore IDISP007 // Don't dispose injected + // 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`!! + // 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 ) diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/ObjectLayer.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/ObjectLayer.cs index 4fde63224..60a0ff630 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/ObjectLayer.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/ObjectLayer.cs @@ -6,6 +6,12 @@ namespace Ubiquity.NET.Llvm.OrcJITv2 { /// ORC JIT v2 Object linking layer + /// + /// 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 { @@ -59,10 +65,10 @@ public void Emit( MaterializationResponsibility resp, MemoryBuffer objBuffer ) } /// - [SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP007:Don't dispose injected", Justification = "Ownership transferred in constructor")] + [SuppressMessage( "IDisposableAnalyzers.Correctness", "IDISP007:Don't dispose injected", Justification = "Ownership transferred in constructor" )] protected override void Dispose( bool disposing ) { - base.Dispose(disposing); + base.Dispose( disposing ); if(!Handle.IsNull) { Handle.Dispose(); diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/RTDyldObjMemoryAllocatorBase.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/RTDyldObjMemoryAllocatorBase.cs deleted file mode 100644 index b886c08ff..000000000 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/RTDyldObjMemoryAllocatorBase.cs +++ /dev/null @@ -1,99 +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 static Ubiquity.NET.Llvm.Interop.ABI.llvm_c.OrcEE; - -namespace Ubiquity.NET.Llvm.OrcJITv2 -{ - /// Base class for an allocator for use as an Object layer in OrcJITv2 - public abstract class RTDyldObjMemoryAllocatorBase - : ObjectLayer - , IJitMemoryAllocator - { - /// Initializes a new instance of the class. - /// Session for this object layer - protected RTDyldObjMemoryAllocatorBase(ExecutionSession session) - : base() - { - AllocatedSelf = new(this); - unsafe - { - Handle = LLVMOrcCreateRTDyldObjectLinkingLayerWithMCJITMemoryManagerLikeCallbacks( - session.Handle, - (void*)AddRefAndGetNativeContext(), - &MemoryAllocatorNativeCallbacks.CreateContext, - &MemoryAllocatorNativeCallbacks.DestroyMemory, // Notify Terminating - &MemoryAllocatorNativeCallbacks.AllocateCodeSection, - &MemoryAllocatorNativeCallbacks.AllocateDataSection, - &MemoryAllocatorNativeCallbacks.FinalizeMemory, - &MemoryAllocatorNativeCallbacks.DestroyContext // NOP - ); - } - } - - /// 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. - /// - /// - public abstract 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. - /// - /// - public abstract 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) - public abstract bool FinalizeMemory([NotNullWhen(false)] out LazyEncodedString? errMsg); - - /// - protected override void Dispose( bool disposing ) - { - if(disposing && !AllocatedSelf.IsInvalid && !AllocatedSelf.IsClosed) - { - // Decrements the ref count on the handle - // might not actually destroy anything - AllocatedSelf.Dispose(); - } - - base.Dispose( disposing ); - } - - internal unsafe nint AddRefAndGetNativeContext( ) - { - ObjectDisposedException.ThrowIf(IsDisposed, this); - - return AllocatedSelf.AddRefAndGetNativeContext(); - } - - // 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; } - } -} diff --git a/src/Ubiquity.NET.Llvm/OrcJITv2/SimpleJitMemoryAllocatorBase.cs b/src/Ubiquity.NET.Llvm/OrcJITv2/SimpleJitMemoryAllocatorBase.cs index 3d8c5afd0..25b834b89 100644 --- a/src/Ubiquity.NET.Llvm/OrcJITv2/SimpleJitMemoryAllocatorBase.cs +++ b/src/Ubiquity.NET.Llvm/OrcJITv2/SimpleJitMemoryAllocatorBase.cs @@ -9,91 +9,84 @@ 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() { - AllocatedSelf = new(this); unsafe { + CallbackContext = this.AsNativeContext(); + Handle = LLVMCreateSimpleMCJITMemoryManager( - (void*)AddRefAndGetNativeContext(), + CallbackContext, &MemoryAllocatorNativeCallbacks.AllocateCodeSection, &MemoryAllocatorNativeCallbacks.AllocateDataSection, &MemoryAllocatorNativeCallbacks.FinalizeMemory, - &MemoryAllocatorNativeCallbacks.DestroyMemory + &MemoryAllocatorNativeCallbacks.NotifyTerminating ); } } - /// 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. - /// - /// + /// public abstract 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. - /// - /// + /// public abstract 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) + /// public abstract bool FinalizeMemory([NotNullWhen(false)] out LazyEncodedString? errMsg); /// - protected override void Dispose( bool disposing ) + public virtual void ReleaseContext() { - if(disposing && !AllocatedSelf.IsInvalid && !AllocatedSelf.IsClosed) + unsafe { - // Decrements the ref count on the handle - // might not actually destroy anything - AllocatedSelf.Dispose(); + NativeContext.Release(ref CallbackContext); } - - Handle = default; - base.Dispose( disposing ); } - internal unsafe nint AddRefAndGetNativeContext( ) + /// + protected override void Dispose( bool disposing ) { - ObjectDisposedException.ThrowIf(IsDisposed, this); + base.Dispose( disposing ); + if(disposing) + { + if(!Handle.IsNull) + { + Handle.Dispose(); + Handle = default; + } - return AllocatedSelf.AddRefAndGetNativeContext(); + // 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); + } + } + } } - // 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; } + 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 } }