From bb13039fb50acc9e2a66b6c4349018133ad3fc23 Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Tue, 26 May 2026 18:16:47 +0000 Subject: [PATCH 01/29] Fix singleton cctor cycle (#3817) by replacing pre-registration with lazy init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-existing design used `SKObject.cctor` to cascade `EnsureStaticInstanceAreInitialized()` calls into every singleton-bearing type, registering each `XxxStatic` subclass into `HandleDictionary` before any user code could run. This pattern broke when one cctor read another's static state — `SKTypeface.cctor` reads `SKFontManager.Default.Handle`, and if `SKFontManager.cctor` was the outermost on the stack the CLR's same-thread-cctor-re-entry rule returned a partially-initialized type with a null `defaultManager` field, throwing `NullReferenceException`. Replace the cascade with lazy init on first property access: - Remove `SKObject.cctor` cascade (keep only the version check). - Drop every `XxxStatic` private subclass (`SKDataStatic`, `SKColorSpaceStatic`, etc.). - Add an `internal SKObject(IntPtr, bool, bool immortal)` ctor that sets `IgnorePublicDispose = true` before `Handle` is assigned, so the wrapper is born immortal — the create-path race where another thread could find the wrapper in `HandleDictionary` and dispose it before the flag is set is structurally closed. - Each singleton-bearing type gets a `GetImmortalObject` helper that uses a factory passing `immortal: true`. - Singleton getters (`SKColorSpace.CreateSrgb`, `SKFontManager.Default`, `SKTypeface.Empty/Default`, `SKData.Empty`, `SKColorFilter.CreateSrgbToLinearGamma`/`CreateLinearToSrgbGamma`, `SKFontStyle.Normal/Bold/Italic/BoldItalic`, `SKPaint.DefaultFont`) cache their wrapper in a static field on first access via `Interlocked.CompareExchange`. - `SKBlender.cctor` keeps its eager 29-mode dictionary but uses the immortal ctor path — no cross-type cctor dependencies remain. - `GarbageCleanupFixture` now identifies singletons by the `IgnorePublicDispose` flag instead of a hardcoded type-name list. - Document the latent `HandleDictionary` stale-entry bug in `SKPath.FlushBuilder`/`ReplaceFromBuilder` (handle reassignment leaks the old HD entry). Remaining narrow race: if a non-immortal wrapper for a singleton's native handle already exists in `HandleDictionary` (e.g., from an `img.ColorSpace` call) when the first singleton-getter call happens, a concurrent `Dispose()` on that existing wrapper can damage it before the post-construction `IgnorePublicDispose = true` set runs. Closing this fully would require either making the lock recursion-aware, holding a write lock across the entire create+flag-set sequence, or refactoring `HandleDictionary.GetOrAddObject` to do the add itself instead of relying on the ctor's Handle-setter side effect. Accepted for now; documented in the design discussion. Co-Authored-By: Claude Opus 4.7 --- binding/SkiaSharp/SKBlender.cs | 37 ++++------- binding/SkiaSharp/SKColorFilter.cs | 54 +++++++-------- binding/SkiaSharp/SKColorSpace.cs | 60 +++++++++-------- binding/SkiaSharp/SKData.cs | 48 ++++++-------- binding/SkiaSharp/SKFont.cs | 5 ++ binding/SkiaSharp/SKFontManager.cs | 46 ++++++------- binding/SkiaSharp/SKFontStyle.cs | 56 +++++++++------- binding/SkiaSharp/SKObject.cs | 24 +++++-- binding/SkiaSharp/SKPaint.cs | 37 ++++++----- binding/SkiaSharp/SKPath.cs | 6 ++ binding/SkiaSharp/SKTypeface.cs | 77 +++++++++++----------- tests/Tests/GarbageCleanupFixture.cs | 15 +---- tests/Tests/SkiaSharp/SKColorFilterTest.cs | 8 ++- tests/Tests/SkiaSharp/SKColorSpaceTest.cs | 29 ++++---- tests/Tests/SkiaSharp/SKFontManagerTest.cs | 63 ++++++++++++++++++ 15 files changed, 310 insertions(+), 255 deletions(-) diff --git a/binding/SkiaSharp/SKBlender.cs b/binding/SkiaSharp/SKBlender.cs index c0b5c811751..5119fa878fc 100644 --- a/binding/SkiaSharp/SKBlender.cs +++ b/binding/SkiaSharp/SKBlender.cs @@ -9,12 +9,8 @@ public unsafe class SKBlender : SKObject, ISKReferenceCounted static SKBlender () { - // TODO: This is not the best way to do this as it will create a lot of objects that - // might not be needed, but it is the only way to ensure that the static - // instances are created before any access is made to them. - // See more info: SKObject.EnsureStaticInstanceAreInitialized() - - // Explicitly list all enum values to avoid reflection (AoT compatibility) + // Explicitly list all enum values to avoid reflection (AoT compatibility). + // 29 fixed entries, no cross-type dependencies, so eager init is fine here. var modes = new SKBlendMode[] { SKBlendMode.Clear, SKBlendMode.Src, @@ -48,20 +44,20 @@ static SKBlender () }; blendModeBlenders = new Dictionary (modes.Length); - foreach (SKBlendMode mode in modes) - { - blendModeBlenders [mode] = new SKBlenderStatic (SkiaApi.sk_blender_new_mode (mode)); + foreach (SKBlendMode mode in modes) { + var blender = GetImmortalObject (SkiaApi.sk_blender_new_mode (mode)); + blender.IgnorePublicDispose = true; + blendModeBlenders[mode] = blender; } } - internal static void EnsureStaticInstanceAreInitialized () + internal SKBlender(IntPtr handle, bool owns) + : base (handle, owns) { - // IMPORTANT: do not remove to ensure that the static instances - // are initialized before any access is made to them } - internal SKBlender(IntPtr handle, bool owns) - : base (handle, owns) + internal SKBlender(IntPtr handle, bool owns, bool immortal) + : base (handle, owns, immortal) { } @@ -81,15 +77,6 @@ public static SKBlender CreateArithmetic (float k1, float k2, float k3, float k4 internal static SKBlender GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKBlender (h, o)); - // - - private sealed class SKBlenderStatic : SKBlender - { - internal SKBlenderStatic (IntPtr x) - : base (x, false) - { - } - - protected override void Dispose (bool disposing) { } - } + internal static SKBlender GetImmortalObject (IntPtr handle) => + GetOrAddObject (handle, (h, o) => new SKBlender (h, o, immortal: true)); } diff --git a/binding/SkiaSharp/SKColorFilter.cs b/binding/SkiaSharp/SKColorFilter.cs index 6abad3dd7d0..b59ed019779 100644 --- a/binding/SkiaSharp/SKColorFilter.cs +++ b/binding/SkiaSharp/SKColorFilter.cs @@ -1,6 +1,7 @@ #nullable disable using System; +using System.Threading; namespace SkiaSharp { @@ -9,37 +10,39 @@ public unsafe class SKColorFilter : SKObject, ISKReferenceCounted public const int ColorMatrixSize = 20; public const int TableMaxLength = 256; - private static readonly SKColorFilter srgbToLinear; - private static readonly SKColorFilter linearToSrgb; + private static SKColorFilter srgbToLinear; + private static SKColorFilter linearToSrgb; - static SKColorFilter () - { - // TODO: This is not the best way to do this as it will create a lot of objects that - // might not be needed, but it is the only way to ensure that the static - // instances are created before any access is made to them. - // See more info: SKObject.EnsureStaticInstanceAreInitialized() - - srgbToLinear = new SKColorFilterStatic (SkiaApi.sk_colorfilter_new_srgb_to_linear_gamma ()); - linearToSrgb = new SKColorFilterStatic (SkiaApi.sk_colorfilter_new_linear_to_srgb_gamma ()); - } - - internal static void EnsureStaticInstanceAreInitialized () + internal SKColorFilter(IntPtr handle, bool owns) + : base (handle, owns) { - // IMPORTANT: do not remove to ensure that the static instances - // are initialized before any access is made to them } - internal SKColorFilter(IntPtr handle, bool owns) - : base (handle, owns) + internal SKColorFilter(IntPtr handle, bool owns, bool immortal) + : base (handle, owns, immortal) { } protected override void Dispose (bool disposing) => base.Dispose (disposing); - public static SKColorFilter CreateSrgbToLinearGamma() => srgbToLinear; + public static SKColorFilter CreateSrgbToLinearGamma () + { + if (srgbToLinear is not null) + return srgbToLinear; + var cf = GetImmortalObject (SkiaApi.sk_colorfilter_new_srgb_to_linear_gamma ()); + cf.IgnorePublicDispose = true; + return Interlocked.CompareExchange (ref srgbToLinear, cf, null) ?? cf; + } - public static SKColorFilter CreateLinearToSrgbGamma() => linearToSrgb; + public static SKColorFilter CreateLinearToSrgbGamma () + { + if (linearToSrgb is not null) + return linearToSrgb; + var cf = GetImmortalObject (SkiaApi.sk_colorfilter_new_linear_to_srgb_gamma ()); + cf.IgnorePublicDispose = true; + return Interlocked.CompareExchange (ref linearToSrgb, cf, null) ?? cf; + } public static SKColorFilter CreateBlendMode(SKColor c, SKBlendMode mode) { @@ -158,15 +161,8 @@ public static SKColorFilter CreateHighContrast(bool grayscale, SKHighContrastCon internal static SKColorFilter GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKColorFilter (h, o)); - - private sealed class SKColorFilterStatic : SKColorFilter - { - internal SKColorFilterStatic (IntPtr x) - : base (x, false) - { - } - protected override void Dispose (bool disposing) { } - } + internal static SKColorFilter GetImmortalObject (IntPtr handle) => + GetOrAddObject (handle, (h, o) => new SKColorFilter (h, o, immortal: true)); } } diff --git a/binding/SkiaSharp/SKColorSpace.cs b/binding/SkiaSharp/SKColorSpace.cs index aa994db762c..b2cdc4fd831 100644 --- a/binding/SkiaSharp/SKColorSpace.cs +++ b/binding/SkiaSharp/SKColorSpace.cs @@ -2,33 +2,22 @@ using System; using System.ComponentModel; +using System.Threading; namespace SkiaSharp { public unsafe class SKColorSpace : SKObject, ISKNonVirtualReferenceCounted { - private static readonly SKColorSpace srgb; - private static readonly SKColorSpace srgbLinear; + private static SKColorSpace srgb; + private static SKColorSpace srgbLinear; - static SKColorSpace () - { - // TODO: This is not the best way to do this as it will create a lot of objects that - // might not be needed, but it is the only way to ensure that the static - // instances are created before any access is made to them. - // See more info: SKObject.EnsureStaticInstanceAreInitialized() - - srgb = new SKColorSpaceStatic (SkiaApi.sk_colorspace_new_srgb ()); - srgbLinear = new SKColorSpaceStatic (SkiaApi.sk_colorspace_new_srgb_linear ()); - } - - internal static void EnsureStaticInstanceAreInitialized () + internal SKColorSpace (IntPtr handle, bool owns) + : base (handle, owns) { - // IMPORTANT: do not remove to ensure that the static instances - // are initialized before any access is made to them } - internal SKColorSpace (IntPtr handle, bool owns) - : base (handle, owns) + internal SKColorSpace (IntPtr handle, bool owns, bool immortal) + : base (handle, owns, immortal) { } @@ -67,11 +56,29 @@ public static bool Equal (SKColorSpace left, SKColorSpace right) // CreateSrgb - public static SKColorSpace CreateSrgb () => srgb; + public static SKColorSpace CreateSrgb () + { + if (srgb is not null) + return srgb; + // immortal-from-ctor: closes the race where another thread could find the + // wrapper in HandleDictionary and dispose it before we set the flag. + var cs = GetImmortalObject (SkiaApi.sk_colorspace_new_srgb ()); + // Promote an existing wrapper to immortal if one was already in HD + // (narrow race remains for that case). + cs.IgnorePublicDispose = true; + return Interlocked.CompareExchange (ref srgb, cs, null) ?? cs; + } // CreateSrgbLinear - public static SKColorSpace CreateSrgbLinear () => srgbLinear; + public static SKColorSpace CreateSrgbLinear () + { + if (srgbLinear is not null) + return srgbLinear; + var cs = GetImmortalObject (SkiaApi.sk_colorspace_new_srgb_linear ()); + cs.IgnorePublicDispose = true; + return Interlocked.CompareExchange (ref srgbLinear, cs, null) ?? cs; + } // CreateIcc @@ -161,14 +168,9 @@ public SKColorSpace ToSrgbGamma () => internal static SKColorSpace GetObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => GetOrAddObject (handle, owns, unrefExisting, (h, o) => new SKColorSpace (h, o)); - private sealed class SKColorSpaceStatic : SKColorSpace - { - internal SKColorSpaceStatic (IntPtr x) - : base (x, false) - { - } - - protected override void Dispose (bool disposing) { } - } + // Variant used by singleton accessors. Newly created wrappers are immortal from birth + // (IgnorePublicDispose set before the wrapper enters HandleDictionary). + internal static SKColorSpace GetImmortalObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => + GetOrAddObject (handle, owns, unrefExisting, (h, o) => new SKColorSpace (h, o, immortal: true)); } } diff --git a/binding/SkiaSharp/SKData.cs b/binding/SkiaSharp/SKData.cs index 6be1ee9846e..83622d9ad80 100644 --- a/binding/SkiaSharp/SKData.cs +++ b/binding/SkiaSharp/SKData.cs @@ -4,6 +4,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using SkiaSharp.Internals; namespace SkiaSharp @@ -15,26 +16,15 @@ public unsafe class SKData : SKObject, ISKNonVirtualReferenceCounted // improvement in Copy performance. internal const int CopyBufferSize = 81920; - private static readonly SKData empty; + private static SKData empty; - static SKData () - { - // TODO: This is not the best way to do this as it will create a lot of objects that - // might not be needed, but it is the only way to ensure that the static - // instances are created before any access is made to them. - // See more info: SKObject.EnsureStaticInstanceAreInitialized() - - empty = new SKDataStatic (SkiaApi.sk_data_new_empty ()); - } - - internal static void EnsureStaticInstanceAreInitialized () + internal SKData (IntPtr x, bool owns) + : base (x, owns) { - // IMPORTANT: do not remove to ensure that the static instances - // are initialized before any access is made to them } - internal SKData (IntPtr x, bool owns) - : base (x, owns) + internal SKData (IntPtr x, bool owns, bool immortal) + : base (x, owns, immortal) { } @@ -45,7 +35,17 @@ protected override void Dispose (bool disposing) => void ISKNonVirtualReferenceCounted.UnreferenceNative () => SkiaApi.sk_data_unref (Handle); - public static SKData Empty => empty; + public static SKData Empty + { + get + { + if (empty is not null) + return empty; + var data = GetImmortalObject (SkiaApi.sk_data_new_empty ()); + data.IgnorePublicDispose = true; + return Interlocked.CompareExchange (ref empty, data, null) ?? data; + } + } // CreateCopy @@ -288,6 +288,9 @@ public void SaveTo (Stream target) internal static SKData GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKData (h, o)); + internal static SKData GetImmortalObject (IntPtr handle) => + GetOrAddObject (handle, (h, o) => new SKData (h, o, immortal: true)); + // private class SKDataStream : UnmanagedMemoryStream @@ -313,16 +316,5 @@ protected override void Dispose (bool disposing) } } - // - - private sealed class SKDataStatic : SKData - { - internal SKDataStatic (IntPtr x) - : base (x, false) - { - } - - protected override void Dispose (bool disposing) { } - } } } diff --git a/binding/SkiaSharp/SKFont.cs b/binding/SkiaSharp/SKFont.cs index 71beb79b96f..b1226d90343 100644 --- a/binding/SkiaSharp/SKFont.cs +++ b/binding/SkiaSharp/SKFont.cs @@ -17,6 +17,11 @@ internal SKFont (IntPtr handle, bool owns) { } + internal SKFont (IntPtr handle, bool owns, bool immortal) + : base (handle, owns, immortal) + { + } + public SKFont () : this (SKTypeface.Default, DefaultSize, DefaultScaleX, DefaultSkewX) { diff --git a/binding/SkiaSharp/SKFontManager.cs b/binding/SkiaSharp/SKFontManager.cs index 184da670674..3e895f06336 100644 --- a/binding/SkiaSharp/SKFontManager.cs +++ b/binding/SkiaSharp/SKFontManager.cs @@ -5,38 +5,38 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading; namespace SkiaSharp { public unsafe class SKFontManager : SKObject, ISKReferenceCounted { - private static readonly SKFontManager defaultManager; + private static SKFontManager defaultManager; - static SKFontManager () - { - // TODO: This is not the best way to do this as it will create a lot of objects that - // might not be needed, but it is the only way to ensure that the static - // instances are created before any access is made to them. - // See more info: SKObject.EnsureStaticInstanceAreInitialized() - - defaultManager = new SKFontManagerStatic (SkiaApi.sk_fontmgr_create_default ()); - } - - internal static void EnsureStaticInstanceAreInitialized () + internal SKFontManager (IntPtr handle, bool owns) + : base (handle, owns) { - // IMPORTANT: do not remove to ensure that the static instances - // are initialized before any access is made to them } - internal SKFontManager (IntPtr handle, bool owns) - : base (handle, owns) + internal SKFontManager (IntPtr handle, bool owns, bool immortal) + : base (handle, owns, immortal) { } protected override void Dispose (bool disposing) => base.Dispose (disposing); - public static SKFontManager Default => defaultManager; + public static SKFontManager Default + { + get + { + if (defaultManager is not null) + return defaultManager; + var fm = GetImmortalObject (SkiaApi.sk_fontmgr_create_default ()); + fm.IgnorePublicDispose = true; + return Interlocked.CompareExchange (ref defaultManager, fm, null) ?? fm; + } + } public int FontFamilyCount => SkiaApi.sk_fontmgr_count_families (Handle); @@ -200,16 +200,8 @@ public static SKFontManager CreateDefault () internal static SKFontManager GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKFontManager (h, o)); - // + internal static SKFontManager GetImmortalObject (IntPtr handle) => + GetOrAddObject (handle, (h, o) => new SKFontManager (h, o, immortal: true)); - private sealed class SKFontManagerStatic : SKFontManager - { - internal SKFontManagerStatic (IntPtr x) - : base (x, false) - { - } - - protected override void Dispose (bool disposing) { } - } } } diff --git a/binding/SkiaSharp/SKFontStyle.cs b/binding/SkiaSharp/SKFontStyle.cs index c1b9ef93c2c..cf80e3d604b 100644 --- a/binding/SkiaSharp/SKFontStyle.cs +++ b/binding/SkiaSharp/SKFontStyle.cs @@ -1,26 +1,24 @@ #nullable disable using System; +using System.Threading; namespace SkiaSharp { public class SKFontStyle : SKObject, ISKSkipObjectRegistration { - private static readonly SKFontStyle normal; - private static readonly SKFontStyle bold; - private static readonly SKFontStyle italic; - private static readonly SKFontStyle boldItalic; + private static SKFontStyle normal; + private static SKFontStyle bold; + private static SKFontStyle italic; + private static SKFontStyle boldItalic; - static SKFontStyle () + internal SKFontStyle (IntPtr handle, bool owns) + : base (handle, owns) { - normal = new SKFontStyleStatic (SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); - bold = new SKFontStyleStatic (SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); - italic = new SKFontStyleStatic (SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Italic); - boldItalic = new SKFontStyleStatic (SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Italic); } - internal SKFontStyle (IntPtr handle, bool owns) - : base (handle, owns) + internal SKFontStyle (IntPtr handle, bool owns, bool immortal) + : base (handle, owns, immortal) { } @@ -39,6 +37,11 @@ public SKFontStyle (int weight, int width, SKFontStyleSlant slant) { } + internal SKFontStyle (SKFontStyleWeight weight, SKFontStyleWidth width, SKFontStyleSlant slant, bool immortal) + : this (SkiaApi.sk_fontstyle_new ((int)weight, (int)width, slant), true, immortal) + { + } + protected override void Dispose (bool disposing) => base.Dispose (disposing); @@ -51,27 +54,30 @@ protected override void DisposeNative () => public SKFontStyleSlant Slant => SkiaApi.sk_fontstyle_get_slant (Handle); - public static SKFontStyle Normal => normal; + public static SKFontStyle Normal => Ensure (ref normal, SKFontStyleWeight.Normal, SKFontStyleSlant.Upright); - public static SKFontStyle Bold => bold; + public static SKFontStyle Bold => Ensure (ref bold, SKFontStyleWeight.Bold, SKFontStyleSlant.Upright); - public static SKFontStyle Italic => italic; + public static SKFontStyle Italic => Ensure (ref italic, SKFontStyleWeight.Normal, SKFontStyleSlant.Italic); - public static SKFontStyle BoldItalic => boldItalic; + public static SKFontStyle BoldItalic => Ensure (ref boldItalic, SKFontStyleWeight.Bold, SKFontStyleSlant.Italic); + + // SKFontStyle is ISKSkipObjectRegistration — HandleDictionary does not dedup, so + // a race here may briefly create a second native object. The loser's wrapper + // goes through normal finalization and unrefs the duplicate native object. + private static SKFontStyle Ensure (ref SKFontStyle slot, SKFontStyleWeight weight, SKFontStyleSlant slant) + { + var existing = slot; + if (existing is not null) + return existing; + // Immortal-from-ctor: IgnorePublicDispose is set before Handle is published. + var style = new SKFontStyle (weight, SKFontStyleWidth.Normal, slant, immortal: true); + return Interlocked.CompareExchange (ref slot, style, null) ?? style; + } // internal static SKFontStyle GetObject (IntPtr handle) => handle == IntPtr.Zero ? null : new SKFontStyle (handle, true); - - private sealed class SKFontStyleStatic : SKFontStyle - { - internal SKFontStyleStatic (SKFontStyleWeight weight, SKFontStyleWidth width, SKFontStyleSlant slant) - : base (weight, width, slant) - { - } - - protected override void Dispose (bool disposing) { } - } } } diff --git a/binding/SkiaSharp/SKObject.cs b/binding/SkiaSharp/SKObject.cs index a8920802513..6b61ec07544 100644 --- a/binding/SkiaSharp/SKObject.cs +++ b/binding/SkiaSharp/SKObject.cs @@ -39,13 +39,6 @@ internal ConcurrentDictionary KeepAliveObjects { static SKObject () { SkiaSharpVersion.CheckNativeLibraryCompatible (true); - - SKColorSpace.EnsureStaticInstanceAreInitialized (); - SKData.EnsureStaticInstanceAreInitialized (); - SKFontManager.EnsureStaticInstanceAreInitialized (); - SKTypeface.EnsureStaticInstanceAreInitialized (); - SKBlender.EnsureStaticInstanceAreInitialized (); - SKColorFilter.EnsureStaticInstanceAreInitialized (); } internal SKObject (IntPtr handle, bool owns) @@ -53,6 +46,11 @@ internal SKObject (IntPtr handle, bool owns) { } + internal SKObject (IntPtr handle, bool owns, bool immortal) + : base (handle, owns, immortal) + { + } + protected override void Dispose (bool disposing) => base.Dispose (disposing); @@ -221,7 +219,19 @@ internal SKNativeObject (IntPtr handle) } internal SKNativeObject (IntPtr handle, bool ownsHandle) + : this (handle, ownsHandle, immortal: false) + { + } + + // Setting `immortal: true` sets IgnorePublicDispose BEFORE Handle is published + // through SKObject.Handle.setter -> RegisterHandle. This closes the race where + // another thread could find the wrapper in HandleDictionary and call Dispose() + // on it between the registration and a post-construction `IgnorePublicDispose = true` + // assignment. See discussion in the singleton init refactor. + internal SKNativeObject (IntPtr handle, bool ownsHandle, bool immortal) { + if (immortal) + IgnorePublicDispose = true; Handle = handle; OwnsHandle = ownsHandle; } diff --git a/binding/SkiaSharp/SKPaint.cs b/binding/SkiaSharp/SKPaint.cs index a100d51a930..41633624046 100644 --- a/binding/SkiaSharp/SKPaint.cs +++ b/binding/SkiaSharp/SKPaint.cs @@ -1,6 +1,7 @@ #nullable disable using System; +using System.Threading; namespace SkiaSharp { @@ -43,16 +44,25 @@ public unsafe class SKPaint : SKObject, ISKSkipObjectRegistration // reset-to-default font. sk_compatpaint_new_with_font / sk_compatpaint_reset // both *copy* the font state into SkCompatPaint::fFont, so this singleton is // never mutated by callers. - private static readonly SKFont defaultFont; + private static SKFont defaultFont; - static SKPaint () + private static SKFont DefaultFont { - defaultFont = new SKFontStatic ( - SkiaApi.sk_font_new_with_values ( - SKTypeface.Default.Handle, - SKFont.DefaultSize, - SKFont.DefaultScaleX, - SKFont.DefaultSkewX)); + get + { + if (defaultFont is not null) + return defaultFont; + // Immortal-from-ctor: IgnorePublicDispose is set before Handle is published. + var font = new SKFont ( + SkiaApi.sk_font_new_with_values ( + SKTypeface.Default.Handle, + SKFont.DefaultSize, + SKFont.DefaultScaleX, + SKFont.DefaultSkewX), + owns: true, + immortal: true); + return Interlocked.CompareExchange (ref defaultFont, font, null) ?? font; + } } internal SKPaint (IntPtr handle, bool owns) @@ -61,7 +71,7 @@ internal SKPaint (IntPtr handle, bool owns) } public SKPaint () - : this (SkiaApi.sk_compatpaint_new_with_font (defaultFont.Handle), true) + : this (SkiaApi.sk_compatpaint_new_with_font (DefaultFont.Handle), true) { if (Handle == IntPtr.Zero) { throw new InvalidOperationException ("Unable to create a new SKPaint instance."); @@ -90,14 +100,7 @@ protected override void DisposeNative () => // Reset public void Reset () => - SkiaApi.sk_compatpaint_reset (Handle, defaultFont.Handle); - - private sealed class SKFontStatic : SKFont - { - internal SKFontStatic (IntPtr handle) : base (handle, false) { } - - protected override void Dispose (bool disposing) { } - } + SkiaApi.sk_compatpaint_reset (Handle, DefaultFont.Handle); // properties diff --git a/binding/SkiaSharp/SKPath.cs b/binding/SkiaSharp/SKPath.cs index 85091075007..e5921918599 100644 --- a/binding/SkiaSharp/SKPath.cs +++ b/binding/SkiaSharp/SKPath.cs @@ -401,6 +401,12 @@ private void EnsureBuilder () _builder = new SKPathBuilder (this); } + // LATENT BUG: FlushBuilder/ReplaceFromBuilder swap the native handle but only + // the *new* handle is added to HandleDictionary (via the Handle setter side + // effect). The *old* handle's HD entry is not removed and lingers as a stale + // WeakReference until this wrapper is GC'd. If Skia ever reuses the old handle + // address for a different object, HD lookup for that handle would return this + // wrapper. Fix would require DeregisterHandle(Handle, this) before reassignment. private void FlushBuilder () { if (_builder == null) diff --git a/binding/SkiaSharp/SKTypeface.cs b/binding/SkiaSharp/SKTypeface.cs index 46dfff81fcd..ca24c2c66c3 100644 --- a/binding/SkiaSharp/SKTypeface.cs +++ b/binding/SkiaSharp/SKTypeface.cs @@ -3,44 +3,24 @@ using System; using System.ComponentModel; using System.IO; +using System.Threading; namespace SkiaSharp { public unsafe class SKTypeface : SKObject, ISKReferenceCounted { - private static readonly SKTypeface empty; - private static readonly SKTypeface defaultTypeface; + private static SKTypeface empty; + private static SKTypeface defaultTypeface; private SKFont font; - static SKTypeface () - { - // TODO: This is not the best way to do this as it will create a lot of objects that - // might not be needed, but it is the only way to ensure that the static - // instances are created before any access is made to them. - // See more info: SKObject.EnsureStaticInstanceAreInitialized() - - empty = new SKTypefaceStatic (SkiaApi.sk_typeface_create_empty ()); - - // Use legacyMakeTypeface(null) to get the platform default — this uses - // fDefaultStyleSet on Android (which searches "sans-serif", "Roboto", - // then falls back to style set 0). matchFamilyStyle(null) doesn't work - // on Android/NDK/Custom because onMatchFamily(null) returns null. - var matched = SkiaApi.sk_fontmgr_legacy_create_typeface ( - SKFontManager.Default.Handle, IntPtr.Zero, SKFontStyle.Normal.Handle); - defaultTypeface = matched == IntPtr.Zero - ? empty - : new SKTypefaceStatic (matched); - } - - internal static void EnsureStaticInstanceAreInitialized () + internal SKTypeface (IntPtr handle, bool owns) + : base (handle, owns) { - // IMPORTANT: do not remove to ensure that the static instances - // are initialized before any access is made to them } - internal SKTypeface (IntPtr handle, bool owns) - : base (handle, owns) + internal SKTypeface (IntPtr handle, bool owns, bool immortal) + : base (handle, owns, immortal) { } @@ -49,9 +29,36 @@ internal SKTypeface (IntPtr handle, bool owns) protected override void Dispose (bool disposing) => base.Dispose (disposing); - public static SKTypeface Default => defaultTypeface; + public static SKTypeface Default + { + get + { + if (defaultTypeface is not null) + return defaultTypeface; + + // Use legacyMakeTypeface(null) to get the platform default — this uses + // fDefaultStyleSet on Android (which searches "sans-serif", "Roboto", + // then falls back to style set 0). matchFamilyStyle(null) doesn't work + // on Android/NDK/Custom because onMatchFamily(null) returns null. + var matched = SkiaApi.sk_fontmgr_legacy_create_typeface ( + SKFontManager.Default.Handle, IntPtr.Zero, SKFontStyle.Normal.Handle); + var tf = matched == IntPtr.Zero ? Empty : GetImmortalObject (matched); + tf.IgnorePublicDispose = true; + return Interlocked.CompareExchange (ref defaultTypeface, tf, null) ?? tf; + } + } - public static SKTypeface Empty => empty; + public static SKTypeface Empty + { + get + { + if (empty is not null) + return empty; + var tf = GetImmortalObject (SkiaApi.sk_typeface_create_empty ()); + tf.IgnorePublicDispose = true; + return Interlocked.CompareExchange (ref empty, tf, null) ?? tf; + } + } public bool IsEmpty => GlyphCount == 0; @@ -447,16 +454,8 @@ public SKTypeface Clone (SKFontArguments args) internal static SKTypeface GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKTypeface (h, o)); - // - - private sealed class SKTypefaceStatic : SKTypeface - { - internal SKTypefaceStatic (IntPtr x) - : base (x, false) - { - } + internal static SKTypeface GetImmortalObject (IntPtr handle) => + GetOrAddObject (handle, (h, o) => new SKTypeface (h, o, immortal: true)); - protected override void Dispose (bool disposing) { } - } } } diff --git a/tests/Tests/GarbageCleanupFixture.cs b/tests/Tests/GarbageCleanupFixture.cs index d8f17fc9930..56ddd507094 100644 --- a/tests/Tests/GarbageCleanupFixture.cs +++ b/tests/Tests/GarbageCleanupFixture.cs @@ -10,17 +10,6 @@ namespace SkiaSharp.Tests { public class GarbageCleanupFixture : IDisposable { - private static readonly string[] StaticTypes = new[] { - "SkiaSharp.SKData+SKDataStatic", - "SkiaSharp.SKFontManager+SKFontManagerStatic", - "SkiaSharp.SKFontStyle+SKFontStyleStatic", - "SkiaSharp.SKTypeface+SKTypefaceStatic", - "SkiaSharp.SKColorSpace+SKColorSpaceStatic", - "SkiaSharp.SKColorFilter+SKColorFilterStatic", - "SkiaSharp.SKBlender+SKBlenderStatic", - "SkiaSharp.SKPaint+SKFontStatic", - }; - public GarbageCleanupFixture() { } @@ -74,7 +63,9 @@ private bool IsExpectedToBeDead(object instance) var skobject = Assert.IsAssignableFrom(instance); - if (StaticTypes.Contains(skobject.GetType().FullName)) + // Immortal singleton wrappers are marked with IgnorePublicDispose = true + // and live for the process lifetime. + if (skobject.IgnorePublicDispose) return false; return true; diff --git a/tests/Tests/SkiaSharp/SKColorFilterTest.cs b/tests/Tests/SkiaSharp/SKColorFilterTest.cs index beff081f6e5..1fb7d2c77da 100644 --- a/tests/Tests/SkiaSharp/SKColorFilterTest.cs +++ b/tests/Tests/SkiaSharp/SKColorFilterTest.cs @@ -11,11 +11,13 @@ public class SKColorFilterTest : SKTest [Trait(Traits.Category.Key, Traits.Category.Values.Smoke)] public void StaticSrgbToLinearIsReturnedAsTheStaticInstance() { + var expected = SKColorFilter.CreateSrgbToLinearGamma(); var handle = SkiaApi.sk_colorfilter_new_srgb_to_linear_gamma(); try { var cs = SKColorFilter.GetObject(handle); - Assert.Equal("SKColorFilterStatic", cs.GetType().Name); + Assert.Same(expected, cs); + Assert.True(cs.IgnorePublicDispose); } finally { @@ -26,11 +28,13 @@ public void StaticSrgbToLinearIsReturnedAsTheStaticInstance() [SkippableFact] public void StaticLinearToSrgbIsReturnedAsTheStaticInstance() { + var expected = SKColorFilter.CreateLinearToSrgbGamma(); var handle = SkiaApi.sk_colorfilter_new_linear_to_srgb_gamma(); try { var cs = SKColorFilter.GetObject(handle); - Assert.Equal("SKColorFilterStatic", cs.GetType().Name); + Assert.Same(expected, cs); + Assert.True(cs.IgnorePublicDispose); } finally { diff --git a/tests/Tests/SkiaSharp/SKColorSpaceTest.cs b/tests/Tests/SkiaSharp/SKColorSpaceTest.cs index 42d58bdaee7..bd53968b607 100644 --- a/tests/Tests/SkiaSharp/SKColorSpaceTest.cs +++ b/tests/Tests/SkiaSharp/SKColorSpaceTest.cs @@ -20,11 +20,13 @@ public void CanCreateSrgb() [SkippableFact] public void StaticSrgbIsReturnedAsTheStaticInstance() { + var expected = SKColorSpace.CreateSrgb(); var handle = SkiaApi.sk_colorspace_new_srgb(); try { var cs = SKColorSpace.GetObject(handle, unrefExisting: false); - Assert.Equal("SKColorSpaceStatic", cs.GetType().Name); + Assert.Same(expected, cs); + Assert.True(cs.IgnorePublicDispose); } finally { @@ -392,40 +394,37 @@ public void CicpPqPngImageHasPqTransferFunction() [SkippableFact] public void SameColorSpaceCreatedDifferentWaysAreTheSameObject() { - // the instance is static, so all instances in .NET are the same instance - const int NativeInstanceCount = 4; - - // get the first instance of the sRGB Linear + // get the first instance of the sRGB Linear and capture the steady-state refcount var colorspace1 = SKColorSpace.CreateSrgbLinear(); - Assert.Equal("SkiaSharp.SKColorSpace+SKColorSpaceStatic", colorspace1.GetType().FullName); - Assert.Equal(NativeInstanceCount, colorspace1.GetReferenceCount()); + Assert.True(colorspace1.IgnorePublicDispose); + var baselineRefCount = colorspace1.GetReferenceCount(); // create a new one with the same parameters, which will return the same instance var colorspace2 = SKColorSpace.CreateRgb(SKColorSpaceTransferFn.Linear, SKColorSpaceXyz.Srgb); - Assert.Equal("SkiaSharp.SKColorSpace+SKColorSpaceStatic", colorspace2.GetType().FullName); + Assert.True(colorspace2.IgnorePublicDispose); Assert.Same(colorspace1, colorspace2); - Assert.Equal(NativeInstanceCount, colorspace1.GetReferenceCount()); - Assert.Equal(NativeInstanceCount, colorspace2.GetReferenceCount()); + Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); + Assert.Equal(baselineRefCount, colorspace2.GetReferenceCount()); // create a different one manually, which will return a new instance var colorspace3 = SKColorSpace.CreateRgb( new SKColorSpaceTransferFn { A = 0.6f, B = 0.5f, C = 0.4f, D = 0.3f, E = 0.2f, F = 0.1f }, SKColorSpaceXyz.Identity); Assert.NotSame(colorspace1, colorspace3); - Assert.Equal(NativeInstanceCount, colorspace1.GetReferenceCount()); - Assert.Equal(NativeInstanceCount, colorspace2.GetReferenceCount()); + Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); + Assert.Equal(baselineRefCount, colorspace2.GetReferenceCount()); colorspace3.Dispose(); Assert.True(colorspace3.IsDisposed); - Assert.Equal(NativeInstanceCount, colorspace1.GetReferenceCount()); + Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); colorspace2.Dispose(); Assert.False(colorspace2.IsDisposed); - Assert.Equal(NativeInstanceCount, colorspace1.GetReferenceCount()); + Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); colorspace1.Dispose(); Assert.False(colorspace1.IsDisposed); - Assert.Equal(NativeInstanceCount, colorspace1.GetReferenceCount()); + Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); } private static void AssertMatrix(float[] expected, SKMatrix44 actual) diff --git a/tests/Tests/SkiaSharp/SKFontManagerTest.cs b/tests/Tests/SkiaSharp/SKFontManagerTest.cs index 72f339a6880..06bb17fcb5d 100644 --- a/tests/Tests/SkiaSharp/SKFontManagerTest.cs +++ b/tests/Tests/SkiaSharp/SKFontManagerTest.cs @@ -1,11 +1,74 @@ using System; using System.IO; +using System.Reflection; +using System.Runtime.Loader; using Xunit; namespace SkiaSharp.Tests { public class SKFontManagerTest : SKTest { + // https://github.com/mono/SkiaSharp/issues/3817 + // When SKFontManager.Default is the FIRST SkiaSharp type touched in a process, + // the static cctor chain (SKFontManager -> SKObject base -> SKTypeface) re-enters + // SKFontManager.Default while its cctor is still running, so defaultManager is + // still null and SKTypeface.cctor throws NullReferenceException, leaving the + // whole chain in a faulted TypeInitializationException state. + // Other tests in the suite usually warm static state via a different entry + // point, so we run the access in an isolated AssemblyLoadContext to get a + // fresh managed-init state. + [SkippableFact] + public void DefaultDoesNotThrowOnFirstAccess() + { + var alc = new IsolatedSkiaSharpLoadContext(typeof(SKFontManager).Assembly); + try + { + var asm = alc.LoadFromAssemblyName(typeof(SKFontManager).Assembly.GetName()); + var type = asm.GetType("SkiaSharp.SKFontManager", throwOnError: true); + var prop = type.GetProperty(nameof(SKFontManager.Default), BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(prop); + + object value; + try + { + value = prop.GetValue(null); + } + catch (TargetInvocationException ex) + { + throw ex.InnerException; + } + + Assert.NotNull(value); + } + finally + { + alc.Unload(); + } + } + + private sealed class IsolatedSkiaSharpLoadContext : AssemblyLoadContext + { + private readonly AssemblyDependencyResolver _resolver; + + public IsolatedSkiaSharpLoadContext(Assembly hostAssembly) + : base("SkiaSharp.Issue3817", isCollectible: true) + { + _resolver = new AssemblyDependencyResolver(hostAssembly.Location); + } + + protected override Assembly Load(AssemblyName assemblyName) + { + var path = _resolver.ResolveAssemblyToPath(assemblyName); + return path != null ? LoadFromAssemblyPath(path) : null; + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + return path != null ? LoadUnmanagedDllFromPath(path) : IntPtr.Zero; + } + } + [Trait(Traits.Category.Key, Traits.Category.Values.MatchCharacter)] [SkippableFact] public void TestFontManagerMatchCharacter() From 2972aa5c0dd19f81a1523bfe67eb09b91cad611a Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Tue, 26 May 2026 21:16:33 +0000 Subject: [PATCH 02/29] Restore [ModuleInitializer] for the native-library version check only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the singleton-init refactor, the version check in SKObject.cctor was running too late: lazy singleton getters do their first P/Invoke (e.g. sk_colorspace_new_srgb) before any SKObject is constructed, so SKObject.cctor hadn't fired by the time a missing or wrong-major-version libSkiaSharp threw EntryPointNotFoundException / DllNotFoundException. Users saw raw P/Invoke failures instead of CheckNativeLibraryCompatible's actionable "incompatible version range [X.0, Y.0)" message. Move the version check into a [ModuleInitializer] so it fires at assembly load, before any user code. Polyfill the attribute for netstandard2.0 / net4x TFMs. This intentionally does NOT pre-register any singleton wrappers — that's what caused #3817 in the first place. The MI body has exactly one line. Co-Authored-By: Claude Opus 4.7 --- binding/SkiaSharp/SKObject.cs | 5 --- .../SkiaSharp/SkiaSharpModuleInitializer.cs | 38 +++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 binding/SkiaSharp/SkiaSharpModuleInitializer.cs diff --git a/binding/SkiaSharp/SKObject.cs b/binding/SkiaSharp/SKObject.cs index 6b61ec07544..9f7018d9557 100644 --- a/binding/SkiaSharp/SKObject.cs +++ b/binding/SkiaSharp/SKObject.cs @@ -36,11 +36,6 @@ internal ConcurrentDictionary KeepAliveObjects { } } - static SKObject () - { - SkiaSharpVersion.CheckNativeLibraryCompatible (true); - } - internal SKObject (IntPtr handle, bool owns) : base (handle, owns) { diff --git a/binding/SkiaSharp/SkiaSharpModuleInitializer.cs b/binding/SkiaSharp/SkiaSharpModuleInitializer.cs new file mode 100644 index 00000000000..246eaa9371e --- /dev/null +++ b/binding/SkiaSharp/SkiaSharpModuleInitializer.cs @@ -0,0 +1,38 @@ +#nullable disable + +using System; +using System.Runtime.CompilerServices; + +#if !NET5_0_OR_GREATER +namespace System.Runtime.CompilerServices +{ + [AttributeUsage (AttributeTargets.Method, Inherited = false)] + internal sealed class ModuleInitializerAttribute : Attribute + { + } +} +#endif + +namespace SkiaSharp +{ + internal static class SkiaSharpModuleInitializer + { + // Runs at assembly load, before any user code. The native version check is + // here (rather than in SKObject.cctor) so it fires before any SkiaApi P/Invoke + // in the lazy singleton getters — otherwise a missing or wrong-version + // libSkiaSharp surfaces as a raw EntryPointNotFoundException from the singleton + // getter's first P/Invoke instead of the more actionable "incompatible version + // range" message from CheckNativeLibraryCompatible. + // + // This file does NOT pre-register any singletons. The singleton-init refactor + // (#3817) intentionally moved that to lazy on first access; reintroducing + // pre-registration here would re-open the cross-cctor cycle. +#pragma warning disable CA2255 // ModuleInitializer in library code is intentional — see comment above. + [ModuleInitializer] +#pragma warning restore CA2255 + internal static void Initialize () + { + SkiaSharpVersion.CheckNativeLibraryCompatible (true); + } + } +} From a9b145b03548108c7c967eeee0a7c8bea150c0d5 Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Tue, 26 May 2026 21:46:06 +0000 Subject: [PATCH 03/29] Narrow promote-existing race by setting IgnorePublicDispose under the HD lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the singleton getter's GetImmortalObject path finds an existing wrapper in HandleDictionary (rather than constructing a new one via the factory), the post-construction `cs.IgnorePublicDispose = true;` in the lazy getter ran *outside* the HD critical section. The window between "wrapper acquired" and "flag set" included a function return, variable assignment, and the field write — wide enough that a concurrent Dispose() on a pre-existing reference could read IgnorePublicDispose=false and proceed with disposal. Push the flag set into HandleDictionary.GetOrAddObject's existing upgradeable read-lock section via a new `bool immortal` parameter. The "promote existing" branch now flips the flag immediately after the wrapper lookup, before the lock is released. This narrows — but does not fully close — the race window: Dispose()'s `if (IgnorePublicDispose) return;` check is itself unlocked, so a read that lands before our set still wins. Fully closing it would require either making the lock recursive and holding write across Dispose()'s flag check, or refactoring to an atomic state machine for the wrapper lifecycle. For the create path (factory invoked), no change — wrappers are still born immortal via the ctor parameter, which closes that race structurally. Drops the redundant post-set in each lazy getter since GetImmortalObject now handles both cases. Co-Authored-By: Claude Opus 4.7 --- binding/SkiaSharp/HandleDictionary.cs | 13 +++++++++++++ binding/SkiaSharp/SKBlender.cs | 6 ++---- binding/SkiaSharp/SKColorFilter.cs | 4 +--- binding/SkiaSharp/SKColorSpace.cs | 15 +++++++-------- binding/SkiaSharp/SKData.cs | 3 +-- binding/SkiaSharp/SKFontManager.cs | 3 +-- binding/SkiaSharp/SKObject.cs | 9 +++++++++ binding/SkiaSharp/SKTypeface.cs | 4 +--- 8 files changed, 35 insertions(+), 22 deletions(-) diff --git a/binding/SkiaSharp/HandleDictionary.cs b/binding/SkiaSharp/HandleDictionary.cs index 45ba20f87d6..2ca61be159f 100644 --- a/binding/SkiaSharp/HandleDictionary.cs +++ b/binding/SkiaSharp/HandleDictionary.cs @@ -56,6 +56,16 @@ internal static bool GetInstance (IntPtr handle, out TSkiaObject in /// /// The instance, or null if the handle was null. internal static TSkiaObject GetOrAddObject (IntPtr handle, bool owns, bool unrefExisting, Func objectFactory) + where TSkiaObject : SKObject => + GetOrAddObject (handle, owns, unrefExisting, immortal: false, objectFactory); + + /// + /// Retrieve or create an instance for the native handle. When is true + /// and an existing wrapper is found, IgnorePublicDispose is set on it inside the critical section + /// — narrowing the promote-existing race window relative to setting it after the call returns. + /// + /// The instance, or null if the handle was null. + internal static TSkiaObject GetOrAddObject (IntPtr handle, bool owns, bool unrefExisting, bool immortal, Func objectFactory) where TSkiaObject : SKObject { if (handle == IntPtr.Zero) @@ -86,6 +96,9 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own refcnt.SafeUnRef (); } + if (immortal) + instance.IgnorePublicDispose = true; + return instance; } diff --git a/binding/SkiaSharp/SKBlender.cs b/binding/SkiaSharp/SKBlender.cs index 5119fa878fc..cf326ab528e 100644 --- a/binding/SkiaSharp/SKBlender.cs +++ b/binding/SkiaSharp/SKBlender.cs @@ -45,9 +45,7 @@ static SKBlender () blendModeBlenders = new Dictionary (modes.Length); foreach (SKBlendMode mode in modes) { - var blender = GetImmortalObject (SkiaApi.sk_blender_new_mode (mode)); - blender.IgnorePublicDispose = true; - blendModeBlenders[mode] = blender; + blendModeBlenders[mode] = GetImmortalObject (SkiaApi.sk_blender_new_mode (mode)); } } @@ -78,5 +76,5 @@ internal static SKBlender GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKBlender (h, o)); internal static SKBlender GetImmortalObject (IntPtr handle) => - GetOrAddObject (handle, (h, o) => new SKBlender (h, o, immortal: true)); + GetOrAddObject (handle, owns: true, unrefExisting: true, immortal: true, (h, o) => new SKBlender (h, o, immortal: true)); } diff --git a/binding/SkiaSharp/SKColorFilter.cs b/binding/SkiaSharp/SKColorFilter.cs index b59ed019779..91fbf389ece 100644 --- a/binding/SkiaSharp/SKColorFilter.cs +++ b/binding/SkiaSharp/SKColorFilter.cs @@ -31,7 +31,6 @@ public static SKColorFilter CreateSrgbToLinearGamma () if (srgbToLinear is not null) return srgbToLinear; var cf = GetImmortalObject (SkiaApi.sk_colorfilter_new_srgb_to_linear_gamma ()); - cf.IgnorePublicDispose = true; return Interlocked.CompareExchange (ref srgbToLinear, cf, null) ?? cf; } @@ -40,7 +39,6 @@ public static SKColorFilter CreateLinearToSrgbGamma () if (linearToSrgb is not null) return linearToSrgb; var cf = GetImmortalObject (SkiaApi.sk_colorfilter_new_linear_to_srgb_gamma ()); - cf.IgnorePublicDispose = true; return Interlocked.CompareExchange (ref linearToSrgb, cf, null) ?? cf; } @@ -163,6 +161,6 @@ internal static SKColorFilter GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKColorFilter (h, o)); internal static SKColorFilter GetImmortalObject (IntPtr handle) => - GetOrAddObject (handle, (h, o) => new SKColorFilter (h, o, immortal: true)); + GetOrAddObject (handle, owns: true, unrefExisting: true, immortal: true, (h, o) => new SKColorFilter (h, o, immortal: true)); } } diff --git a/binding/SkiaSharp/SKColorSpace.cs b/binding/SkiaSharp/SKColorSpace.cs index b2cdc4fd831..5766af02334 100644 --- a/binding/SkiaSharp/SKColorSpace.cs +++ b/binding/SkiaSharp/SKColorSpace.cs @@ -60,12 +60,10 @@ public static SKColorSpace CreateSrgb () { if (srgb is not null) return srgb; - // immortal-from-ctor: closes the race where another thread could find the - // wrapper in HandleDictionary and dispose it before we set the flag. + // Newly-created wrappers are immortal-from-ctor (flag set before Handle + // is registered in HD). For existing wrappers, GetImmortalObject sets + // IgnorePublicDispose inside the HD critical section. var cs = GetImmortalObject (SkiaApi.sk_colorspace_new_srgb ()); - // Promote an existing wrapper to immortal if one was already in HD - // (narrow race remains for that case). - cs.IgnorePublicDispose = true; return Interlocked.CompareExchange (ref srgb, cs, null) ?? cs; } @@ -76,7 +74,6 @@ public static SKColorSpace CreateSrgbLinear () if (srgbLinear is not null) return srgbLinear; var cs = GetImmortalObject (SkiaApi.sk_colorspace_new_srgb_linear ()); - cs.IgnorePublicDispose = true; return Interlocked.CompareExchange (ref srgbLinear, cs, null) ?? cs; } @@ -169,8 +166,10 @@ internal static SKColorSpace GetObject (IntPtr handle, bool owns = true, bool un GetOrAddObject (handle, owns, unrefExisting, (h, o) => new SKColorSpace (h, o)); // Variant used by singleton accessors. Newly created wrappers are immortal from birth - // (IgnorePublicDispose set before the wrapper enters HandleDictionary). + // (IgnorePublicDispose set before the wrapper enters HandleDictionary). Existing + // wrappers are promoted to immortal under the HD lock — narrows the race window + // against a concurrent Dispose() on a reference held from before this call. internal static SKColorSpace GetImmortalObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => - GetOrAddObject (handle, owns, unrefExisting, (h, o) => new SKColorSpace (h, o, immortal: true)); + GetOrAddObject (handle, owns, unrefExisting, immortal: true, (h, o) => new SKColorSpace (h, o, immortal: true)); } } diff --git a/binding/SkiaSharp/SKData.cs b/binding/SkiaSharp/SKData.cs index 83622d9ad80..115b8908dfa 100644 --- a/binding/SkiaSharp/SKData.cs +++ b/binding/SkiaSharp/SKData.cs @@ -42,7 +42,6 @@ public static SKData Empty if (empty is not null) return empty; var data = GetImmortalObject (SkiaApi.sk_data_new_empty ()); - data.IgnorePublicDispose = true; return Interlocked.CompareExchange (ref empty, data, null) ?? data; } } @@ -289,7 +288,7 @@ internal static SKData GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKData (h, o)); internal static SKData GetImmortalObject (IntPtr handle) => - GetOrAddObject (handle, (h, o) => new SKData (h, o, immortal: true)); + GetOrAddObject (handle, owns: true, unrefExisting: true, immortal: true, (h, o) => new SKData (h, o, immortal: true)); // diff --git a/binding/SkiaSharp/SKFontManager.cs b/binding/SkiaSharp/SKFontManager.cs index 3e895f06336..64a8cf46b03 100644 --- a/binding/SkiaSharp/SKFontManager.cs +++ b/binding/SkiaSharp/SKFontManager.cs @@ -33,7 +33,6 @@ public static SKFontManager Default if (defaultManager is not null) return defaultManager; var fm = GetImmortalObject (SkiaApi.sk_fontmgr_create_default ()); - fm.IgnorePublicDispose = true; return Interlocked.CompareExchange (ref defaultManager, fm, null) ?? fm; } } @@ -201,7 +200,7 @@ internal static SKFontManager GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKFontManager (h, o)); internal static SKFontManager GetImmortalObject (IntPtr handle) => - GetOrAddObject (handle, (h, o) => new SKFontManager (h, o, immortal: true)); + GetOrAddObject (handle, owns: true, unrefExisting: true, immortal: true, (h, o) => new SKFontManager (h, o, immortal: true)); } } diff --git a/binding/SkiaSharp/SKObject.cs b/binding/SkiaSharp/SKObject.cs index 9f7018d9557..7c1cddec102 100644 --- a/binding/SkiaSharp/SKObject.cs +++ b/binding/SkiaSharp/SKObject.cs @@ -117,6 +117,15 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own return HandleDictionary.GetOrAddObject (handle, owns, unrefExisting, objectFactory); } + internal static TSkiaObject GetOrAddObject (IntPtr handle, bool owns, bool unrefExisting, bool immortal, Func objectFactory) + where TSkiaObject : SKObject + { + if (handle == IntPtr.Zero) + return null; + + return HandleDictionary.GetOrAddObject (handle, owns, unrefExisting, immortal, objectFactory); + } + internal static void RegisterHandle (IntPtr handle, SKObject instance) { if (handle == IntPtr.Zero || instance == null) diff --git a/binding/SkiaSharp/SKTypeface.cs b/binding/SkiaSharp/SKTypeface.cs index ca24c2c66c3..8aea9b1c9f4 100644 --- a/binding/SkiaSharp/SKTypeface.cs +++ b/binding/SkiaSharp/SKTypeface.cs @@ -43,7 +43,6 @@ public static SKTypeface Default var matched = SkiaApi.sk_fontmgr_legacy_create_typeface ( SKFontManager.Default.Handle, IntPtr.Zero, SKFontStyle.Normal.Handle); var tf = matched == IntPtr.Zero ? Empty : GetImmortalObject (matched); - tf.IgnorePublicDispose = true; return Interlocked.CompareExchange (ref defaultTypeface, tf, null) ?? tf; } } @@ -55,7 +54,6 @@ public static SKTypeface Empty if (empty is not null) return empty; var tf = GetImmortalObject (SkiaApi.sk_typeface_create_empty ()); - tf.IgnorePublicDispose = true; return Interlocked.CompareExchange (ref empty, tf, null) ?? tf; } } @@ -455,7 +453,7 @@ internal static SKTypeface GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKTypeface (h, o)); internal static SKTypeface GetImmortalObject (IntPtr handle) => - GetOrAddObject (handle, (h, o) => new SKTypeface (h, o, immortal: true)); + GetOrAddObject (handle, owns: true, unrefExisting: true, immortal: true, (h, o) => new SKTypeface (h, o, immortal: true)); } } From 707ee98795c437627710cf7cca19f6fdcaefd446 Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Tue, 26 May 2026 21:47:47 +0000 Subject: [PATCH 04/29] Remove incorrect SKPath stale-HD-entry comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SKPath implements ISKSkipObjectRegistration, so RegisterHandle / DeregisterHandle are no-ops for it — there's no HandleDictionary entry that could become stale when FlushBuilder / ReplaceFromBuilder swap the native handle. The comment I added in the previous singleton-init refactor commit was wrong. Co-Authored-By: Claude Opus 4.7 --- binding/SkiaSharp/SKPath.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/binding/SkiaSharp/SKPath.cs b/binding/SkiaSharp/SKPath.cs index e5921918599..85091075007 100644 --- a/binding/SkiaSharp/SKPath.cs +++ b/binding/SkiaSharp/SKPath.cs @@ -401,12 +401,6 @@ private void EnsureBuilder () _builder = new SKPathBuilder (this); } - // LATENT BUG: FlushBuilder/ReplaceFromBuilder swap the native handle but only - // the *new* handle is added to HandleDictionary (via the Handle setter side - // effect). The *old* handle's HD entry is not removed and lingers as a stale - // WeakReference until this wrapper is GC'd. If Skia ever reuses the old handle - // address for a different object, HD lookup for that handle would return this - // wrapper. Fix would require DeregisterHandle(Handle, this) before reassignment. private void FlushBuilder () { if (_builder == null) From e782f52ca379fadb024c049f3ecde53d1a8264ae Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Tue, 26 May 2026 21:52:19 +0000 Subject: [PATCH 05/29] Make IgnorePublicDispose a one-way latch via private setter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demote IgnorePublicDispose's setter to `private` and route the only cross-type write site (HandleDictionary.GetOrAddObject's promote-existing branch) through the existing PreventPublicDisposal() method. Move PreventPublicDisposal() down from SKObject to SKNativeObject so it lives on the same class that declares the property. Before: any code in the binding (or any InternalsVisibleTo'd subproject) could write `wrapper.IgnorePublicDispose = false` and re-arm an immortal singleton for disposal. Nothing in the tree does it today, but the type system didn't prevent a future contributor from accidentally re-introducing the footgun. After: the property can be observed widely but only set to `true` via PreventPublicDisposal() — no path to flip it back to `false` short of modifying SKNativeObject itself. Immortality becomes a one-way latch. Co-Authored-By: Claude Opus 4.7 --- binding/SkiaSharp/HandleDictionary.cs | 2 +- binding/SkiaSharp/SKObject.cs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/binding/SkiaSharp/HandleDictionary.cs b/binding/SkiaSharp/HandleDictionary.cs index 2ca61be159f..9bf94671224 100644 --- a/binding/SkiaSharp/HandleDictionary.cs +++ b/binding/SkiaSharp/HandleDictionary.cs @@ -97,7 +97,7 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own } if (immortal) - instance.IgnorePublicDispose = true; + instance.PreventPublicDisposal (); return instance; } diff --git a/binding/SkiaSharp/SKObject.cs b/binding/SkiaSharp/SKObject.cs index 7c1cddec102..7a024a36804 100644 --- a/binding/SkiaSharp/SKObject.cs +++ b/binding/SkiaSharp/SKObject.cs @@ -153,18 +153,12 @@ internal static bool GetInstance (IntPtr handle, out TSkiaObject in return HandleDictionary.GetInstance (handle, out instance); } - // indicate that the user cannot dispose the object - internal void PreventPublicDisposal () - { - IgnorePublicDispose = true; - } - // indicate that the ownership of this object is now in the hands of // the native object internal void RevokeOwnership (SKObject newOwner) { OwnsHandle = false; - IgnorePublicDispose = true; + PreventPublicDisposal (); if (newOwner == null) DisposeInternal (); @@ -251,7 +245,16 @@ internal SKNativeObject (IntPtr handle, bool ownsHandle, bool immortal) protected internal virtual bool OwnsHandle { get; protected set; } - protected internal bool IgnorePublicDispose { get; set; } + // One-way latch: once set to true, only stays true. Demoted setter prevents + // future contributors from accidentally flipping it back to false on a wrapper + // that's relying on it for immortality. Use PreventPublicDisposal() to set. + protected internal bool IgnorePublicDispose { get; private set; } + + // Make this wrapper undisposable via the public Dispose() method. + internal void PreventPublicDisposal () + { + IgnorePublicDispose = true; + } protected internal bool IsDisposed => isDisposed == 1; From 008f4bfab60559a6204c9ee24c3b7e822ca78f8d Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Tue, 26 May 2026 22:03:22 +0000 Subject: [PATCH 06/29] Eager-init SKFontStyle singletons via cctor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SKFontStyle was using a lazy-init pattern (`Ensure(ref slot, ...)` + CAS) that had a benign race: sk_fontstyle_new allocates a fresh native object every call, so concurrent first-access could create two natives before the CAS settled, leaking the loser's allocation until the GC's finalizer reclaimed it. Replace with `static readonly` field initializers. SKFontStyle is uniquely safe for eager static init among the singleton-bearing types because: - It's ISKSkipObjectRegistration — no HandleDictionary interaction, no risk of the kind of cross-cctor cycle that #3817 exposed. - Its ctor only reads enum constants — no cross-type static state. - Skia's sk_fontstyle_new doesn't share singleton handles, so HandleDictionary dedup wouldn't help us avoid the race even if it were registered there. The CLR's once-per-AppDomain cctor guarantee gives us free thread-safe single-allocation semantics. Removes the Ensure helper and the System.Threading using. Co-Authored-By: Claude Opus 4.7 --- binding/SkiaSharp/SKFontStyle.cs | 40 ++++++++++++++------------------ 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/binding/SkiaSharp/SKFontStyle.cs b/binding/SkiaSharp/SKFontStyle.cs index cf80e3d604b..e4cf225093d 100644 --- a/binding/SkiaSharp/SKFontStyle.cs +++ b/binding/SkiaSharp/SKFontStyle.cs @@ -1,16 +1,25 @@ #nullable disable using System; -using System.Threading; namespace SkiaSharp { public class SKFontStyle : SKObject, ISKSkipObjectRegistration { - private static SKFontStyle normal; - private static SKFontStyle bold; - private static SKFontStyle italic; - private static SKFontStyle boldItalic; + // Eager init via field initializers (cctor) is safe here: SKFontStyle is + // ISKSkipObjectRegistration (no HandleDictionary dedup risk), the ctor only + // reads enum constants (no cross-singleton dependencies), and the CLR's + // once-per-AppDomain cctor guarantee removes the race that lazy init would + // have for this type (sk_fontstyle_new allocates a fresh native object every + // call, so a concurrent lazy init would briefly leak the loser's allocation). + private static readonly SKFontStyle normal = + new SKFontStyle (SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, immortal: true); + private static readonly SKFontStyle bold = + new SKFontStyle (SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, immortal: true); + private static readonly SKFontStyle italic = + new SKFontStyle (SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Italic, immortal: true); + private static readonly SKFontStyle boldItalic = + new SKFontStyle (SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Italic, immortal: true); internal SKFontStyle (IntPtr handle, bool owns) : base (handle, owns) @@ -54,26 +63,13 @@ protected override void DisposeNative () => public SKFontStyleSlant Slant => SkiaApi.sk_fontstyle_get_slant (Handle); - public static SKFontStyle Normal => Ensure (ref normal, SKFontStyleWeight.Normal, SKFontStyleSlant.Upright); + public static SKFontStyle Normal => normal; - public static SKFontStyle Bold => Ensure (ref bold, SKFontStyleWeight.Bold, SKFontStyleSlant.Upright); + public static SKFontStyle Bold => bold; - public static SKFontStyle Italic => Ensure (ref italic, SKFontStyleWeight.Normal, SKFontStyleSlant.Italic); + public static SKFontStyle Italic => italic; - public static SKFontStyle BoldItalic => Ensure (ref boldItalic, SKFontStyleWeight.Bold, SKFontStyleSlant.Italic); - - // SKFontStyle is ISKSkipObjectRegistration — HandleDictionary does not dedup, so - // a race here may briefly create a second native object. The loser's wrapper - // goes through normal finalization and unrefs the duplicate native object. - private static SKFontStyle Ensure (ref SKFontStyle slot, SKFontStyleWeight weight, SKFontStyleSlant slant) - { - var existing = slot; - if (existing is not null) - return existing; - // Immortal-from-ctor: IgnorePublicDispose is set before Handle is published. - var style = new SKFontStyle (weight, SKFontStyleWidth.Normal, slant, immortal: true); - return Interlocked.CompareExchange (ref slot, style, null) ?? style; - } + public static SKFontStyle BoldItalic => boldItalic; // From 0b017197efb84295a9c7e7cad4d577f9da67b532 Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Wed, 27 May 2026 02:01:01 +0000 Subject: [PATCH 07/29] =?UTF-8?q?Atomicize=20singleton=20init=20via=20recu?= =?UTF-8?q?rsive=20HD=20lock;=20rename=20immortal=E2=86=92dispose-protecte?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the final piece of the singleton-init refactor (#3817). Five sub-changes, all interrelated enough to land together: 1. Atomic state + HD-state change via recursive lock. PlatformLock now uses LockRecursionPolicy.SupportsRecursion (matches the Win32 CRITICAL_SECTION path which has always been recursive). Public Dispose() takes the HD write lock around its IgnorePublicDispose check + isDisposed CAS, and PreventPublicDisposal() takes the same write lock around its flag set. The two operations are mutually exclusive — exactly one wins. The race window from earlier commit 000e29e is now fully closed, not just narrowed. Dispose(bool)'s CAS guard moved up to each entry point (Dispose, DisposeInternal, finalizer) so the CAS can be paired atomically with whatever check it needs. The body is now pure cleanup, called at most once per wrapper by virtue of each entry point's own CAS. 2. IsDisposed uses Volatile.Read for ARM64 correctness. GetInstanceNoLocks's !IsDisposed filter is now load-bearing for race correctness (it's what makes T1's lookup miss a wrapper that T2 just CAS'd to disposed). Plain int reads can be reordered past the Interlocked CAS on weak memory models; Volatile.Read pins the read-after-write happens-before. 3. Drop the per-ctor `immortal` bool parameter. The original purpose was to set IgnorePublicDispose before Handle was published to HD. With the new lock contract, GetOrAddObject can call PreventPublicDisposal inside its upgradeable read lock (recursively upgrading to write), so the flag gets set under the lock instead of in the ctor. Wrappers no longer need to know whether they're a singleton at construction time. Saves one ctor overload per singleton-bearing type (~6 ctors gone). 4. Replace RevokeOwnership with TransferOwnershipToNative. RevokeOwnership(newOwner) had two paths: dispose-immediately (newOwner=null) or keep-wrapper-alive-via-parent.OwnedObjects (newOwner!=null). The kept-alive path tested an implementation detail — the wrapper was 'alive but unusable' between codec/typeface Create and codec/typeface Dispose. Switching to dispose-immediately makes the contract loud: after passing a stream to SKCodec.Create or SKFontManager.CreateTypeface, the stream wrapper is disposed immediately. Subsequent use fails with a clear Handle=0 instead of silently operating on memory that codec/typeface now owns. Three test classes updated (SKManagedStreamTest, SKCodecTest, SKTypefaceTest) to match the new contract. 5. Switch all lazy singletons to the locking LazyInitializer overload. sk_fontmgr_create_default and sk_font_new_with_values allocate fresh native objects on each call (not Skia-side singletons), so the basic LazyInitializer.EnsureInitialized(ref, factory) form's 'may run factory multiple times under contention' would leak orphan native objects per losing thread. The locking overload (ref bool + ref object) guarantees the factory runs exactly once. Applied uniformly to all seven lazy singletons for consistency, even the HD-deduped ones that don't strictly need it. 6. Rename 'immortal' → 'dispose-protected' throughout. 'Immortal' was a misnomer: these wrappers ARE GC-collectable (finalizer runs Dispose(false), which still tears down). What the IgnorePublicDispose flag actually does is short-circuit public Dispose(). The wrappers' real persistence comes from their static-field cache acting as a GC root, not from the flag. Renames: GetImmortalObject → GetDisposeProtectedObject, GetOrAddImmortalObject → GetOrAddDisposeProtectedObject, MakeImmortal → MakeDisposeProtected (in SKFontStyle), bool immortal → bool disposeProtected (HandleDictionary param). Co-Authored-By: Claude Opus 4.7 --- binding/SkiaSharp/HandleDictionary.cs | 26 +++- binding/SkiaSharp/PlatformLock.cs | 19 +-- binding/SkiaSharp/SKBlender.cs | 12 +- binding/SkiaSharp/SKCodec.cs | 2 +- binding/SkiaSharp/SKColorFilter.cs | 36 ++--- binding/SkiaSharp/SKColorSpace.cs | 46 +++--- binding/SkiaSharp/SKData.cs | 25 ++-- binding/SkiaSharp/SKFont.cs | 5 - binding/SkiaSharp/SKFontManager.cs | 27 ++-- binding/SkiaSharp/SKFontStyle.cs | 29 ++-- binding/SkiaSharp/SKObject.cs | 134 ++++++++++++------ binding/SkiaSharp/SKPaint.cs | 36 +++-- binding/SkiaSharp/SKTypeface.cs | 59 ++++---- .../SkiaSharp/SkiaSharpModuleInitializer.cs | 12 +- tests/Tests/GarbageCleanupFixture.cs | 2 +- tests/Tests/SkiaSharp/SKCodecTest.cs | 23 ++- tests/Tests/SkiaSharp/SKManagedStreamTest.cs | 28 ++-- tests/Tests/SkiaSharp/SKTypefaceTest.cs | 24 ++-- 18 files changed, 270 insertions(+), 275 deletions(-) diff --git a/binding/SkiaSharp/HandleDictionary.cs b/binding/SkiaSharp/HandleDictionary.cs index 9bf94671224..f17c2dd2475 100644 --- a/binding/SkiaSharp/HandleDictionary.cs +++ b/binding/SkiaSharp/HandleDictionary.cs @@ -57,15 +57,18 @@ internal static bool GetInstance (IntPtr handle, out TSkiaObject in /// The instance, or null if the handle was null. internal static TSkiaObject GetOrAddObject (IntPtr handle, bool owns, bool unrefExisting, Func objectFactory) where TSkiaObject : SKObject => - GetOrAddObject (handle, owns, unrefExisting, immortal: false, objectFactory); + GetOrAddObject (handle, owns, unrefExisting, disposeProtected: false, objectFactory); /// - /// Retrieve or create an instance for the native handle. When is true - /// and an existing wrapper is found, IgnorePublicDispose is set on it inside the critical section - /// — narrowing the promote-existing race window relative to setting it after the call returns. + /// Retrieve or create an instance for the native handle. When is true + /// and an existing wrapper is found, IgnorePublicDispose is set on it via PreventPublicDisposal, + /// which acquires the HD write lock — so the flag set is mutually exclusive with any concurrent + /// Dispose() (which also holds the write lock). Combined with the recursive lock policy that + /// lets Dispose() hold the write lock across both its state change and the Handle setter's + /// DeregisterHandle, this eliminates the promote-existing race entirely. /// /// The instance, or null if the handle was null. - internal static TSkiaObject GetOrAddObject (IntPtr handle, bool owns, bool unrefExisting, bool immortal, Func objectFactory) + internal static TSkiaObject GetOrAddObject (IntPtr handle, bool owns, bool unrefExisting, bool disposeProtected, Func objectFactory) where TSkiaObject : SKObject { if (handle == IntPtr.Zero) @@ -96,7 +99,7 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own refcnt.SafeUnRef (); } - if (immortal) + if (disposeProtected) instance.PreventPublicDisposal (); return instance; @@ -104,6 +107,17 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own var obj = objectFactory.Invoke (handle, owns); + // Mark the freshly-created wrapper disposeProtected *before* releasing the lock. + // The wrapper is briefly HD-observable (RegisterHandle ran inside the + // factory's ctor) between this point and PreventPublicDisposal setting + // the flag. A concurrent thread that finds W via HD lookup can only + // destructively act on it through public Dispose(), which takes the + // write lock and blocks until we exit upgradeable read — by which time + // PreventPublicDisposal has run. DisposeInternal/RevokeOwnership paths + // are not reachable for a wrapper that hasn't yet escaped this factory. + if (disposeProtected && obj is not null) + obj.PreventPublicDisposal (); + return obj; } finally { instancesLock.ExitUpgradeableReadLock (); diff --git a/binding/SkiaSharp/PlatformLock.cs b/binding/SkiaSharp/PlatformLock.cs index 9a191802ee1..a01fb6c9560 100644 --- a/binding/SkiaSharp/PlatformLock.cs +++ b/binding/SkiaSharp/PlatformLock.cs @@ -7,14 +7,14 @@ /* * This is a fix for issue #1383. - * + * * https://github.com/mono/SkiaSharp/issues/1383 - * - * On Windows, .NET locks are alertable when using the STA threading model and can - * cause the Windows message loop to be dispatched (typically on WM_PAINT messages). - * This can lead to re-entrancy and a deadlock on the HandleDictionary lock. - * - * This fix replaces the ReaderWriteLockSlim instance on Windows with a native Win32 + * + * On Windows, .NET locks are alertable when using the STA threading model and can + * cause the Windows message loop to be dispatched (typically on WM_PAINT messages). + * This can lead to re-entrancy and a deadlock on the HandleDictionary lock. + * + * This fix replaces the ReaderWriteLockSlim instance on Windows with a native Win32 * CRITICAL_SECTION. */ @@ -89,7 +89,10 @@ class ReadWriteLock : IPlatformLock public void EnterUpgradeableReadLock () => _lock.EnterUpgradeableReadLock (); public void ExitUpgradeableReadLock () => _lock.ExitUpgradeableReadLock (); - ReaderWriterLockSlim _lock = new ReaderWriterLockSlim (); + // SupportsRecursion: lets Dispose() take the write lock at the top of the flow + // and have the Handle setter's DeregisterHandle re-enter that same lock when + // Handle is zeroed at the end. + ReaderWriterLockSlim _lock = new ReaderWriterLockSlim (LockRecursionPolicy.SupportsRecursion); } #if !(__IOS__ || __TVOS__ || __MACOS__ || __MACCATALYST__ || __ANDROID__) diff --git a/binding/SkiaSharp/SKBlender.cs b/binding/SkiaSharp/SKBlender.cs index cf326ab528e..a1549ef7891 100644 --- a/binding/SkiaSharp/SKBlender.cs +++ b/binding/SkiaSharp/SKBlender.cs @@ -10,7 +10,6 @@ public unsafe class SKBlender : SKObject, ISKReferenceCounted static SKBlender () { // Explicitly list all enum values to avoid reflection (AoT compatibility). - // 29 fixed entries, no cross-type dependencies, so eager init is fine here. var modes = new SKBlendMode[] { SKBlendMode.Clear, SKBlendMode.Src, @@ -45,7 +44,7 @@ static SKBlender () blendModeBlenders = new Dictionary (modes.Length); foreach (SKBlendMode mode in modes) { - blendModeBlenders[mode] = GetImmortalObject (SkiaApi.sk_blender_new_mode (mode)); + blendModeBlenders[mode] = GetDisposeProtectedObject (SkiaApi.sk_blender_new_mode (mode)); } } @@ -54,11 +53,6 @@ internal SKBlender(IntPtr handle, bool owns) { } - internal SKBlender(IntPtr handle, bool owns, bool immortal) - : base (handle, owns, immortal) - { - } - protected override void Dispose (bool disposing) => base.Dispose (disposing); @@ -75,6 +69,6 @@ public static SKBlender CreateArithmetic (float k1, float k2, float k3, float k4 internal static SKBlender GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKBlender (h, o)); - internal static SKBlender GetImmortalObject (IntPtr handle) => - GetOrAddObject (handle, owns: true, unrefExisting: true, immortal: true, (h, o) => new SKBlender (h, o, immortal: true)); + internal static SKBlender GetDisposeProtectedObject (IntPtr handle) => + GetOrAddDisposeProtectedObject (handle, owns: true, unrefExisting: true, (h, o) => new SKBlender (h, o)); } diff --git a/binding/SkiaSharp/SKCodec.cs b/binding/SkiaSharp/SKCodec.cs index 859f8b7825a..c0b6e2e4ac7 100644 --- a/binding/SkiaSharp/SKCodec.cs +++ b/binding/SkiaSharp/SKCodec.cs @@ -258,7 +258,7 @@ public static SKCodec Create (SKStream stream, out SKCodecResult result) fixed (SKCodecResult* r = &result) { var codec = GetObject (SkiaApi.sk_codec_new_from_stream (stream.Handle, r)); - stream.RevokeOwnership (codec); + stream.TransferOwnershipToNative (); return codec; } } diff --git a/binding/SkiaSharp/SKColorFilter.cs b/binding/SkiaSharp/SKColorFilter.cs index 91fbf389ece..5af57d4c25f 100644 --- a/binding/SkiaSharp/SKColorFilter.cs +++ b/binding/SkiaSharp/SKColorFilter.cs @@ -11,36 +11,30 @@ public unsafe class SKColorFilter : SKObject, ISKReferenceCounted public const int TableMaxLength = 256; private static SKColorFilter srgbToLinear; + private static bool srgbToLinearInitialized; + private static object srgbToLinearLock = new object (); + private static SKColorFilter linearToSrgb; + private static bool linearToSrgbInitialized; + private static object linearToSrgbLock = new object (); internal SKColorFilter(IntPtr handle, bool owns) : base (handle, owns) { } - internal SKColorFilter(IntPtr handle, bool owns, bool immortal) - : base (handle, owns, immortal) - { - } - protected override void Dispose (bool disposing) => base.Dispose (disposing); - public static SKColorFilter CreateSrgbToLinearGamma () - { - if (srgbToLinear is not null) - return srgbToLinear; - var cf = GetImmortalObject (SkiaApi.sk_colorfilter_new_srgb_to_linear_gamma ()); - return Interlocked.CompareExchange (ref srgbToLinear, cf, null) ?? cf; - } + public static SKColorFilter CreateSrgbToLinearGamma () => + LazyInitializer.EnsureInitialized ( + ref srgbToLinear, ref srgbToLinearInitialized, ref srgbToLinearLock, + () => GetDisposeProtectedObject (SkiaApi.sk_colorfilter_new_srgb_to_linear_gamma ())); - public static SKColorFilter CreateLinearToSrgbGamma () - { - if (linearToSrgb is not null) - return linearToSrgb; - var cf = GetImmortalObject (SkiaApi.sk_colorfilter_new_linear_to_srgb_gamma ()); - return Interlocked.CompareExchange (ref linearToSrgb, cf, null) ?? cf; - } + public static SKColorFilter CreateLinearToSrgbGamma () => + LazyInitializer.EnsureInitialized ( + ref linearToSrgb, ref linearToSrgbInitialized, ref linearToSrgbLock, + () => GetDisposeProtectedObject (SkiaApi.sk_colorfilter_new_linear_to_srgb_gamma ())); public static SKColorFilter CreateBlendMode(SKColor c, SKBlendMode mode) { @@ -160,7 +154,7 @@ public static SKColorFilter CreateHighContrast(bool grayscale, SKHighContrastCon internal static SKColorFilter GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKColorFilter (h, o)); - internal static SKColorFilter GetImmortalObject (IntPtr handle) => - GetOrAddObject (handle, owns: true, unrefExisting: true, immortal: true, (h, o) => new SKColorFilter (h, o, immortal: true)); + internal static SKColorFilter GetDisposeProtectedObject (IntPtr handle) => + GetOrAddDisposeProtectedObject (handle, owns: true, unrefExisting: true, (h, o) => new SKColorFilter (h, o)); } } diff --git a/binding/SkiaSharp/SKColorSpace.cs b/binding/SkiaSharp/SKColorSpace.cs index 5766af02334..c8e78f975fa 100644 --- a/binding/SkiaSharp/SKColorSpace.cs +++ b/binding/SkiaSharp/SKColorSpace.cs @@ -9,18 +9,18 @@ namespace SkiaSharp public unsafe class SKColorSpace : SKObject, ISKNonVirtualReferenceCounted { private static SKColorSpace srgb; + private static bool srgbInitialized; + private static object srgbLock = new object (); + private static SKColorSpace srgbLinear; + private static bool srgbLinearInitialized; + private static object srgbLinearLock = new object (); internal SKColorSpace (IntPtr handle, bool owns) : base (handle, owns) { } - internal SKColorSpace (IntPtr handle, bool owns, bool immortal) - : base (handle, owns, immortal) - { - } - void ISKNonVirtualReferenceCounted.ReferenceNative () => SkiaApi.sk_colorspace_ref (Handle); @@ -56,26 +56,17 @@ public static bool Equal (SKColorSpace left, SKColorSpace right) // CreateSrgb - public static SKColorSpace CreateSrgb () - { - if (srgb is not null) - return srgb; - // Newly-created wrappers are immortal-from-ctor (flag set before Handle - // is registered in HD). For existing wrappers, GetImmortalObject sets - // IgnorePublicDispose inside the HD critical section. - var cs = GetImmortalObject (SkiaApi.sk_colorspace_new_srgb ()); - return Interlocked.CompareExchange (ref srgb, cs, null) ?? cs; - } + public static SKColorSpace CreateSrgb () => + LazyInitializer.EnsureInitialized ( + ref srgb, ref srgbInitialized, ref srgbLock, + () => GetDisposeProtectedObject (SkiaApi.sk_colorspace_new_srgb ())); // CreateSrgbLinear - public static SKColorSpace CreateSrgbLinear () - { - if (srgbLinear is not null) - return srgbLinear; - var cs = GetImmortalObject (SkiaApi.sk_colorspace_new_srgb_linear ()); - return Interlocked.CompareExchange (ref srgbLinear, cs, null) ?? cs; - } + public static SKColorSpace CreateSrgbLinear () => + LazyInitializer.EnsureInitialized ( + ref srgbLinear, ref srgbLinearInitialized, ref srgbLinearLock, + () => GetDisposeProtectedObject (SkiaApi.sk_colorspace_new_srgb_linear ())); // CreateIcc @@ -165,11 +156,10 @@ public SKColorSpace ToSrgbGamma () => internal static SKColorSpace GetObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => GetOrAddObject (handle, owns, unrefExisting, (h, o) => new SKColorSpace (h, o)); - // Variant used by singleton accessors. Newly created wrappers are immortal from birth - // (IgnorePublicDispose set before the wrapper enters HandleDictionary). Existing - // wrappers are promoted to immortal under the HD lock — narrows the race window - // against a concurrent Dispose() on a reference held from before this call. - internal static SKColorSpace GetImmortalObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => - GetOrAddObject (handle, owns, unrefExisting, immortal: true, (h, o) => new SKColorSpace (h, o, immortal: true)); + // Variant used by singleton accessors. The returned wrapper has IgnorePublicDispose + // set under HandleDictionary's critical section — atomic with the HD lookup, so + // no other thread can observe a non-dispose-protected state. + internal static SKColorSpace GetDisposeProtectedObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => + GetOrAddDisposeProtectedObject (handle, owns, unrefExisting, (h, o) => new SKColorSpace (h, o)); } } diff --git a/binding/SkiaSharp/SKData.cs b/binding/SkiaSharp/SKData.cs index 115b8908dfa..1c159842891 100644 --- a/binding/SkiaSharp/SKData.cs +++ b/binding/SkiaSharp/SKData.cs @@ -17,17 +17,14 @@ public unsafe class SKData : SKObject, ISKNonVirtualReferenceCounted internal const int CopyBufferSize = 81920; private static SKData empty; + private static bool emptyInitialized; + private static object emptyLock = new object (); internal SKData (IntPtr x, bool owns) : base (x, owns) { } - internal SKData (IntPtr x, bool owns, bool immortal) - : base (x, owns, immortal) - { - } - protected override void Dispose (bool disposing) => base.Dispose (disposing); @@ -35,16 +32,10 @@ protected override void Dispose (bool disposing) => void ISKNonVirtualReferenceCounted.UnreferenceNative () => SkiaApi.sk_data_unref (Handle); - public static SKData Empty - { - get - { - if (empty is not null) - return empty; - var data = GetImmortalObject (SkiaApi.sk_data_new_empty ()); - return Interlocked.CompareExchange (ref empty, data, null) ?? data; - } - } + public static SKData Empty => + LazyInitializer.EnsureInitialized ( + ref empty, ref emptyInitialized, ref emptyLock, + () => GetDisposeProtectedObject (SkiaApi.sk_data_new_empty ())); // CreateCopy @@ -287,8 +278,8 @@ public void SaveTo (Stream target) internal static SKData GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKData (h, o)); - internal static SKData GetImmortalObject (IntPtr handle) => - GetOrAddObject (handle, owns: true, unrefExisting: true, immortal: true, (h, o) => new SKData (h, o, immortal: true)); + internal static SKData GetDisposeProtectedObject (IntPtr handle) => + GetOrAddDisposeProtectedObject (handle, owns: true, unrefExisting: true, (h, o) => new SKData (h, o)); // diff --git a/binding/SkiaSharp/SKFont.cs b/binding/SkiaSharp/SKFont.cs index b1226d90343..71beb79b96f 100644 --- a/binding/SkiaSharp/SKFont.cs +++ b/binding/SkiaSharp/SKFont.cs @@ -17,11 +17,6 @@ internal SKFont (IntPtr handle, bool owns) { } - internal SKFont (IntPtr handle, bool owns, bool immortal) - : base (handle, owns, immortal) - { - } - public SKFont () : this (SKTypeface.Default, DefaultSize, DefaultScaleX, DefaultSkewX) { diff --git a/binding/SkiaSharp/SKFontManager.cs b/binding/SkiaSharp/SKFontManager.cs index 64a8cf46b03..172fdde2e26 100644 --- a/binding/SkiaSharp/SKFontManager.cs +++ b/binding/SkiaSharp/SKFontManager.cs @@ -12,30 +12,21 @@ namespace SkiaSharp public unsafe class SKFontManager : SKObject, ISKReferenceCounted { private static SKFontManager defaultManager; + private static bool defaultManagerInitialized; + private static object defaultManagerLock = new object (); internal SKFontManager (IntPtr handle, bool owns) : base (handle, owns) { } - internal SKFontManager (IntPtr handle, bool owns, bool immortal) - : base (handle, owns, immortal) - { - } - protected override void Dispose (bool disposing) => base.Dispose (disposing); - public static SKFontManager Default - { - get - { - if (defaultManager is not null) - return defaultManager; - var fm = GetImmortalObject (SkiaApi.sk_fontmgr_create_default ()); - return Interlocked.CompareExchange (ref defaultManager, fm, null) ?? fm; - } - } + public static SKFontManager Default => + LazyInitializer.EnsureInitialized ( + ref defaultManager, ref defaultManagerInitialized, ref defaultManagerLock, + () => GetDisposeProtectedObject (SkiaApi.sk_fontmgr_create_default ())); public int FontFamilyCount => SkiaApi.sk_fontmgr_count_families (Handle); @@ -115,7 +106,7 @@ public SKTypeface CreateTypeface (SKStreamAsset stream, int index = 0) } var typeface = SKTypeface.GetObject (SkiaApi.sk_fontmgr_create_from_stream (Handle, stream.Handle, index)); - stream.RevokeOwnership (typeface); + stream.TransferOwnershipToNative (); return typeface; } @@ -199,8 +190,8 @@ public static SKFontManager CreateDefault () internal static SKFontManager GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKFontManager (h, o)); - internal static SKFontManager GetImmortalObject (IntPtr handle) => - GetOrAddObject (handle, owns: true, unrefExisting: true, immortal: true, (h, o) => new SKFontManager (h, o, immortal: true)); + internal static SKFontManager GetDisposeProtectedObject (IntPtr handle) => + GetOrAddDisposeProtectedObject (handle, owns: true, unrefExisting: true, (h, o) => new SKFontManager (h, o)); } } diff --git a/binding/SkiaSharp/SKFontStyle.cs b/binding/SkiaSharp/SKFontStyle.cs index e4cf225093d..c6715039e78 100644 --- a/binding/SkiaSharp/SKFontStyle.cs +++ b/binding/SkiaSharp/SKFontStyle.cs @@ -6,28 +6,24 @@ namespace SkiaSharp { public class SKFontStyle : SKObject, ISKSkipObjectRegistration { - // Eager init via field initializers (cctor) is safe here: SKFontStyle is - // ISKSkipObjectRegistration (no HandleDictionary dedup risk), the ctor only - // reads enum constants (no cross-singleton dependencies), and the CLR's - // once-per-AppDomain cctor guarantee removes the race that lazy init would - // have for this type (sk_fontstyle_new allocates a fresh native object every - // call, so a concurrent lazy init would briefly leak the loser's allocation). private static readonly SKFontStyle normal = - new SKFontStyle (SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, immortal: true); + MakeDisposeProtected (SKFontStyleWeight.Normal, SKFontStyleSlant.Upright); private static readonly SKFontStyle bold = - new SKFontStyle (SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, immortal: true); + MakeDisposeProtected (SKFontStyleWeight.Bold, SKFontStyleSlant.Upright); private static readonly SKFontStyle italic = - new SKFontStyle (SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Italic, immortal: true); + MakeDisposeProtected (SKFontStyleWeight.Normal, SKFontStyleSlant.Italic); private static readonly SKFontStyle boldItalic = - new SKFontStyle (SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Italic, immortal: true); + MakeDisposeProtected (SKFontStyleWeight.Bold, SKFontStyleSlant.Italic); - internal SKFontStyle (IntPtr handle, bool owns) - : base (handle, owns) + private static SKFontStyle MakeDisposeProtected (SKFontStyleWeight weight, SKFontStyleSlant slant) { + var style = new SKFontStyle (weight, SKFontStyleWidth.Normal, slant); + style.PreventPublicDisposal (); + return style; } - internal SKFontStyle (IntPtr handle, bool owns, bool immortal) - : base (handle, owns, immortal) + internal SKFontStyle (IntPtr handle, bool owns) + : base (handle, owns) { } @@ -46,11 +42,6 @@ public SKFontStyle (int weight, int width, SKFontStyleSlant slant) { } - internal SKFontStyle (SKFontStyleWeight weight, SKFontStyleWidth width, SKFontStyleSlant slant, bool immortal) - : this (SkiaApi.sk_fontstyle_new ((int)weight, (int)width, slant), true, immortal) - { - } - protected override void Dispose (bool disposing) => base.Dispose (disposing); diff --git a/binding/SkiaSharp/SKObject.cs b/binding/SkiaSharp/SKObject.cs index 7a024a36804..5e8f419e31e 100644 --- a/binding/SkiaSharp/SKObject.cs +++ b/binding/SkiaSharp/SKObject.cs @@ -41,11 +41,6 @@ internal SKObject (IntPtr handle, bool owns) { } - internal SKObject (IntPtr handle, bool owns, bool immortal) - : base (handle, owns, immortal) - { - } - protected override void Dispose (bool disposing) => base.Dispose (disposing); @@ -117,13 +112,20 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own return HandleDictionary.GetOrAddObject (handle, owns, unrefExisting, objectFactory); } - internal static TSkiaObject GetOrAddObject (IntPtr handle, bool owns, bool unrefExisting, bool immortal, Func objectFactory) + // Variant that promotes the returned wrapper to dispose-protected + // (IgnorePublicDispose = true) inside HandleDictionary's critical section. + // Used by the singleton accessors (CreateSrgb, Default, etc.). "Dispose-protected" + // means the public Dispose() is short-circuited — the wrapper is NOT immortal + // from GC's perspective; finalization and DisposeInternal still tear it down. + // The actual long-lived persistence comes from each accessor's static-field + // cache acting as a GC root. + internal static TSkiaObject GetOrAddDisposeProtectedObject (IntPtr handle, bool owns, bool unrefExisting, Func objectFactory) where TSkiaObject : SKObject { if (handle == IntPtr.Zero) return null; - return HandleDictionary.GetOrAddObject (handle, owns, unrefExisting, immortal, objectFactory); + return HandleDictionary.GetOrAddObject (handle, owns, unrefExisting, disposeProtected: true, objectFactory); } internal static void RegisterHandle (IntPtr handle, SKObject instance) @@ -153,19 +155,6 @@ internal static bool GetInstance (IntPtr handle, out TSkiaObject in return HandleDictionary.GetInstance (handle, out instance); } - // indicate that the ownership of this object is now in the hands of - // the native object - internal void RevokeOwnership (SKObject newOwner) - { - OwnsHandle = false; - PreventPublicDisposal (); - - if (newOwner == null) - DisposeInternal (); - else - newOwner.OwnedObjects[Handle] = this; - } - // indicate that the child is controlled by the native code and // the managed wrapper should be disposed when the owner is internal static T OwnedBy (T child, SKObject owner) @@ -217,19 +206,7 @@ internal SKNativeObject (IntPtr handle) } internal SKNativeObject (IntPtr handle, bool ownsHandle) - : this (handle, ownsHandle, immortal: false) { - } - - // Setting `immortal: true` sets IgnorePublicDispose BEFORE Handle is published - // through SKObject.Handle.setter -> RegisterHandle. This closes the race where - // another thread could find the wrapper in HandleDictionary and call Dispose() - // on it between the registration and a post-construction `IgnorePublicDispose = true` - // assignment. See discussion in the singleton init refactor. - internal SKNativeObject (IntPtr handle, bool ownsHandle, bool immortal) - { - if (immortal) - IgnorePublicDispose = true; Handle = handle; OwnsHandle = ownsHandle; } @@ -238,6 +215,11 @@ internal SKNativeObject (IntPtr handle, bool ownsHandle, bool immortal) { fromFinalizer = true; + // Claim disposal here (the CAS used to be inside Dispose(bool), but that's + // moved out so each entry point gates atomically with whatever check it + // pairs with). No outer HD lock — same reasoning as DisposeInternal. + if (Interlocked.CompareExchange (ref isDisposed, 1, 0) != 0) + return; Dispose (false); } @@ -247,16 +229,35 @@ internal SKNativeObject (IntPtr handle, bool ownsHandle, bool immortal) // One-way latch: once set to true, only stays true. Demoted setter prevents // future contributors from accidentally flipping it back to false on a wrapper - // that's relying on it for immortality. Use PreventPublicDisposal() to set. + // that's relying on it for dispose protection. Use PreventPublicDisposal() to set. protected internal bool IgnorePublicDispose { get; private set; } - // Make this wrapper undisposable via the public Dispose() method. + // Make this wrapper unreachable via the public Dispose() method. + // Acquires the HD write lock so the flag set is serialized with any concurrent + // Dispose() (which holds the same lock around its check). Atomic with respect + // to "is this wrapper still in HD" — closes the promote-existing race in + // singleton init (#3817 discussion thread). internal void PreventPublicDisposal () { - IgnorePublicDispose = true; + // Unlocked fast path: if the flag is already set, no work to do. A stale-read + // miss on weak memory models just means we enter the lock and set the flag + // again (idempotent). + if (IgnorePublicDispose) + return; + + HandleDictionary.instancesLock.EnterWriteLock (); + try { + IgnorePublicDispose = true; + } finally { + HandleDictionary.instancesLock.ExitWriteLock (); + } } - protected internal bool IsDisposed => isDisposed == 1; + // Volatile.Read for acquire semantics on weak memory models (ARM/ARM64). The + // post-refactor design relies on HandleDictionary.GetInstanceNoLocks reading + // this property to filter out disposed wrappers, so a stale read could let a + // disposed wrapper escape the filter and become the cached singleton. + protected internal bool IsDisposed => Volatile.Read (ref isDisposed) == 1; protected virtual void DisposeUnownedManaged () { @@ -273,11 +274,13 @@ protected virtual void DisposeNative () // dispose of any unmanaged resources } + // The isDisposed CAS that used to guard this method has moved to the public + // entry points (Dispose, DisposeInternal, finalizer) so that each entry's CAS + // can be paired atomically with whatever check it needs (e.g. Dispose's + // IgnorePublicDispose check under the HD write lock). This method now assumes + // the caller has already claimed the disposal — it is the cleanup body only. protected virtual void Dispose (bool disposing) { - if (Interlocked.CompareExchange (ref isDisposed, 1, 0) != 0) - return; - // dispose any objects that are owned/created by native code if (disposing) DisposeUnownedManaged (); @@ -295,17 +298,66 @@ protected virtual void Dispose (bool disposing) public void Dispose () { - if (IgnorePublicDispose) + // Hold the HD write lock only across the flag check + the isDisposed CAS. + // This pairs the read of IgnorePublicDispose with the disposal claim + // atomically: a concurrent PreventPublicDisposal (which takes the same + // write lock) is mutually exclusive with this section. + // + // The actual cleanup runs *outside* the lock. That's safe because: + // 1. If a concurrent thread is in GetOrAddObject looking up this handle + // *after* our CAS landed, GetInstanceNoLocks filters out wrappers with + // IsDisposed=true — it returns false and the caller falls into the + // factory branch, producing a fresh wrapper. + // 2. RegisterHandle's replacement branch only fires for !IsDisposed + // existing entries, so a fresh wrapper registering for this handle + // while we're still mid-cleanup just overwrites our stale weak ref + // without trying to dispose us recursively. + // 3. Our own Handle = 0 → DeregisterHandle path correctly handles + // "weak.Target is now someone else" (no-op in release). + HandleDictionary.instancesLock.EnterWriteLock (); + bool proceed; + try { + if (IgnorePublicDispose) + return; + proceed = Interlocked.CompareExchange (ref isDisposed, 1, 0) == 0; + } finally { + HandleDictionary.instancesLock.ExitWriteLock (); + } + + if (!proceed) return; - DisposeInternal (); + Dispose (true); + GC.SuppressFinalize (this); } protected internal void DisposeInternal () { + // Claim disposal via the CAS; if already claimed, no-op. No outer HD lock — + // DisposeInternal doesn't read IgnorePublicDispose, so there's no flag + // check that needs to be paired with the CAS. + if (Interlocked.CompareExchange (ref isDisposed, 1, 0) != 0) + return; Dispose (true); GC.SuppressFinalize (this); } + + // Hand off ownership of the native object to native-side code that took it + // (e.g. SKCodec.Create / SKFontManager.CreateTypeface, where the C wrapper + // puts the stream into a unique_ptr that the codec/typeface then owns). + // Marks the wrapper as non-owning so the disposal won't unref the native, + // then disposes the managed wrapper. The user's reference to this wrapper + // becomes a disposed wrapper; subsequent use fails loudly rather than + // operating on a native object that's now owned elsewhere. + // + // Order matters: OwnsHandle = false MUST precede DisposeInternal. A racing + // Dispose() that sees OwnsHandle = true would call DisposeNative and free + // the native object that the receiving native code is about to use. + internal void TransferOwnershipToNative () + { + OwnsHandle = false; + DisposeInternal (); + } } internal static class SKObjectExtensions diff --git a/binding/SkiaSharp/SKPaint.cs b/binding/SkiaSharp/SKPaint.cs index 41633624046..a6816ae343d 100644 --- a/binding/SkiaSharp/SKPaint.cs +++ b/binding/SkiaSharp/SKPaint.cs @@ -45,25 +45,23 @@ public unsafe class SKPaint : SKObject, ISKSkipObjectRegistration // both *copy* the font state into SkCompatPaint::fFont, so this singleton is // never mutated by callers. private static SKFont defaultFont; - - private static SKFont DefaultFont - { - get - { - if (defaultFont is not null) - return defaultFont; - // Immortal-from-ctor: IgnorePublicDispose is set before Handle is published. - var font = new SKFont ( - SkiaApi.sk_font_new_with_values ( - SKTypeface.Default.Handle, - SKFont.DefaultSize, - SKFont.DefaultScaleX, - SKFont.DefaultSkewX), - owns: true, - immortal: true); - return Interlocked.CompareExchange (ref defaultFont, font, null) ?? font; - } - } + private static bool defaultFontInitialized; + private static object defaultFontLock = new object (); + + private static SKFont DefaultFont => + LazyInitializer.EnsureInitialized ( + ref defaultFont, ref defaultFontInitialized, ref defaultFontLock, + () => { + var font = new SKFont ( + SkiaApi.sk_font_new_with_values ( + SKTypeface.Default.Handle, + SKFonxt.DefaultSize, + SKFont.DefaultScaleX, + SKFont.DefaultSkewX), + owns: true); + font.PreventPublicDisposal (); + return font; + }); internal SKPaint (IntPtr handle, bool owns) : base (handle, owns) diff --git a/binding/SkiaSharp/SKTypeface.cs b/binding/SkiaSharp/SKTypeface.cs index 8aea9b1c9f4..eba4fb270d3 100644 --- a/binding/SkiaSharp/SKTypeface.cs +++ b/binding/SkiaSharp/SKTypeface.cs @@ -10,7 +10,12 @@ namespace SkiaSharp public unsafe class SKTypeface : SKObject, ISKReferenceCounted { private static SKTypeface empty; + private static bool emptyInitialized; + private static object emptyLock = new object (); + private static SKTypeface defaultTypeface; + private static bool defaultTypefaceInitialized; + private static object defaultTypefaceLock = new object (); private SKFont font; @@ -19,44 +24,28 @@ internal SKTypeface (IntPtr handle, bool owns) { } - internal SKTypeface (IntPtr handle, bool owns, bool immortal) - : base (handle, owns, immortal) - { - } - // Default protected override void Dispose (bool disposing) => base.Dispose (disposing); - public static SKTypeface Default - { - get - { - if (defaultTypeface is not null) - return defaultTypeface; - - // Use legacyMakeTypeface(null) to get the platform default — this uses - // fDefaultStyleSet on Android (which searches "sans-serif", "Roboto", - // then falls back to style set 0). matchFamilyStyle(null) doesn't work - // on Android/NDK/Custom because onMatchFamily(null) returns null. - var matched = SkiaApi.sk_fontmgr_legacy_create_typeface ( - SKFontManager.Default.Handle, IntPtr.Zero, SKFontStyle.Normal.Handle); - var tf = matched == IntPtr.Zero ? Empty : GetImmortalObject (matched); - return Interlocked.CompareExchange (ref defaultTypeface, tf, null) ?? tf; - } - } - - public static SKTypeface Empty - { - get - { - if (empty is not null) - return empty; - var tf = GetImmortalObject (SkiaApi.sk_typeface_create_empty ()); - return Interlocked.CompareExchange (ref empty, tf, null) ?? tf; - } - } + public static SKTypeface Default => + LazyInitializer.EnsureInitialized ( + ref defaultTypeface, ref defaultTypefaceInitialized, ref defaultTypefaceLock, + () => { + // Use legacyMakeTypeface(null) to get the platform default — this uses + // fDefaultStyleSet on Android (which searches "sans-serif", "Roboto", + // then falls back to style set 0). matchFamilyStyle(null) doesn't work + // on Android/NDK/Custom because onMatchFamily(null) returns null. + var matched = SkiaApi.sk_fontmgr_legacy_create_typeface ( + SKFontManager.Default.Handle, IntPtr.Zero, SKFontStyle.Normal.Handle); + return matched == IntPtr.Zero ? Empty : GetDisposeProtectedObject (matched); + }); + + public static SKTypeface Empty => + LazyInitializer.EnsureInitialized ( + ref empty, ref emptyInitialized, ref emptyLock, + () => GetDisposeProtectedObject (SkiaApi.sk_typeface_create_empty ())); public bool IsEmpty => GlyphCount == 0; @@ -452,8 +441,8 @@ public SKTypeface Clone (SKFontArguments args) internal static SKTypeface GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKTypeface (h, o)); - internal static SKTypeface GetImmortalObject (IntPtr handle) => - GetOrAddObject (handle, owns: true, unrefExisting: true, immortal: true, (h, o) => new SKTypeface (h, o, immortal: true)); + internal static SKTypeface GetDisposeProtectedObject (IntPtr handle) => + GetOrAddDisposeProtectedObject (handle, owns: true, unrefExisting: true, (h, o) => new SKTypeface (h, o)); } } diff --git a/binding/SkiaSharp/SkiaSharpModuleInitializer.cs b/binding/SkiaSharp/SkiaSharpModuleInitializer.cs index 246eaa9371e..792f9766213 100644 --- a/binding/SkiaSharp/SkiaSharpModuleInitializer.cs +++ b/binding/SkiaSharp/SkiaSharpModuleInitializer.cs @@ -17,17 +17,7 @@ namespace SkiaSharp { internal static class SkiaSharpModuleInitializer { - // Runs at assembly load, before any user code. The native version check is - // here (rather than in SKObject.cctor) so it fires before any SkiaApi P/Invoke - // in the lazy singleton getters — otherwise a missing or wrong-version - // libSkiaSharp surfaces as a raw EntryPointNotFoundException from the singleton - // getter's first P/Invoke instead of the more actionable "incompatible version - // range" message from CheckNativeLibraryCompatible. - // - // This file does NOT pre-register any singletons. The singleton-init refactor - // (#3817) intentionally moved that to lazy on first access; reintroducing - // pre-registration here would re-open the cross-cctor cycle. -#pragma warning disable CA2255 // ModuleInitializer in library code is intentional — see comment above. +#pragma warning disable CA2255 // ModuleInitializer in library code is intentional and acceptable in this case [ModuleInitializer] #pragma warning restore CA2255 internal static void Initialize () diff --git a/tests/Tests/GarbageCleanupFixture.cs b/tests/Tests/GarbageCleanupFixture.cs index 56ddd507094..33c139afe5c 100644 --- a/tests/Tests/GarbageCleanupFixture.cs +++ b/tests/Tests/GarbageCleanupFixture.cs @@ -63,7 +63,7 @@ private bool IsExpectedToBeDead(object instance) var skobject = Assert.IsAssignableFrom(instance); - // Immortal singleton wrappers are marked with IgnorePublicDispose = true + // Dispose-protected singleton wrappers are marked with IgnorePublicDispose = true // and live for the process lifetime. if (skobject.IgnorePublicDispose) return false; diff --git a/tests/Tests/SkiaSharp/SKCodecTest.cs b/tests/Tests/SkiaSharp/SKCodecTest.cs index 4e5d6035479..0f6e3c3c526 100644 --- a/tests/Tests/SkiaSharp/SKCodecTest.cs +++ b/tests/Tests/SkiaSharp/SKCodecTest.cs @@ -79,29 +79,28 @@ public unsafe void StreamLosesOwnershipToCodecButIsNotForgotten() } [SkippableFact] - public unsafe void StreamLosesOwnershipAndCanBeDisposedButIsNotActually() + public unsafe void StreamIsDisposedAfterOwnershipTransfer() { var path = Path.Combine(PathToImages, "color-wheel.png"); var stream = new SKMemoryStream(File.ReadAllBytes(path)); var handle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IgnorePublicDispose); + Assert.False(stream.IsDisposed); Assert.True(SKObject.GetInstance(handle, out _)); var codec = SKCodec.Create(stream); + Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); + Assert.True(stream.IsDisposed); + Assert.False(SKObject.GetInstance(handle, out _)); stream.Dispose(); - Assert.True(SKObject.GetInstance(handle, out var inst)); - Assert.Same(stream, inst); Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels)); Assert.NotEmpty(pixels); codec.Dispose(); - Assert.False(SKObject.GetInstance(handle, out _)); } [SkippableFact] @@ -111,13 +110,13 @@ public unsafe void InvalidStreamIsDisposedImmediately() var handle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IgnorePublicDispose); + Assert.False(stream.IsDisposed); Assert.True(SKObject.GetInstance(handle, out _)); Assert.Null(SKCodec.Create(stream)); Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); + Assert.True(stream.IsDisposed); Assert.False(SKObject.GetInstance(handle, out _)); } @@ -145,9 +144,9 @@ void DoWork(out IntPtr codecHandle, out IntPtr streamHandle) Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels)); Assert.NotEmpty(pixels); - Assert.True(SKObject.GetInstance(streamHandle, out var stream)); - Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); + // The managed stream wrapper was disposed at the ownership transfer + // inside SKCodec.Create — the native stream is owned by the codec now. + Assert.False(SKObject.GetInstance(streamHandle, out _)); } SKCodec CreateCodec(out IntPtr streamHandle) @@ -156,7 +155,7 @@ SKCodec CreateCodec(out IntPtr streamHandle) streamHandle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IgnorePublicDispose); + Assert.False(stream.IsDisposed); Assert.True(SKObject.GetInstance(streamHandle, out _)); return SKCodec.Create(stream); diff --git a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs index 94071469874..d57af832080 100644 --- a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs +++ b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs @@ -322,7 +322,7 @@ SKDocument CreateDocument(out IntPtr streamHandle) } [SkippableFact] - public unsafe void StreamLosesOwnershipAndCanBeDisposedButIsNotActually() + public unsafe void StreamIsDisposedAfterOwnershipTransfer() { var path = Path.Combine(PathToImages, "color-wheel.png"); var bytes = File.ReadAllBytes(path); @@ -330,22 +330,26 @@ public unsafe void StreamLosesOwnershipAndCanBeDisposedButIsNotActually() var handle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IgnorePublicDispose); + Assert.False(stream.IsDisposed); Assert.True(SKObject.GetInstance(handle, out _)); var codec = SKCodec.Create(stream); + + // After ownership transfer the managed wrapper is disposed: the native + // stream is now owned by the codec, so the wrapper must not hold an + // outstanding ref or be available for further use. Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); + Assert.True(stream.IsDisposed); + Assert.False(SKObject.GetInstance(handle, out _)); + // Disposing the already-disposed wrapper is a no-op. stream.Dispose(); - Assert.True(SKObject.GetInstance(handle, out var inst)); - Assert.Same(stream, inst); + // Codec still works — it owns the native stream now. Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels)); Assert.NotEmpty(pixels); codec.Dispose(); - Assert.False(SKObject.GetInstance(handle, out _)); } [SkippableFact] @@ -355,13 +359,13 @@ public unsafe void InvalidStreamIsDisposedImmediately() var handle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IgnorePublicDispose); + Assert.False(stream.IsDisposed); Assert.True(SKObject.GetInstance(handle, out _)); Assert.Null(SKCodec.Create(stream)); Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); + Assert.True(stream.IsDisposed); Assert.False(SKObject.GetInstance(handle, out _)); } @@ -389,9 +393,9 @@ void DoWork(out IntPtr codecHandle, out IntPtr streamHandle) Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels)); Assert.NotEmpty(pixels); - Assert.True(SKObject.GetInstance(streamHandle, out var stream)); - Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); + // The managed stream wrapper was disposed at the ownership transfer + // inside SKCodec.Create — the native stream is owned by the codec now. + Assert.False(SKObject.GetInstance(streamHandle, out _)); } SKCodec CreateCodec(out IntPtr streamHandle) @@ -400,7 +404,7 @@ SKCodec CreateCodec(out IntPtr streamHandle) streamHandle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IgnorePublicDispose); + Assert.False(stream.IsDisposed); Assert.True(SKObject.GetInstance(streamHandle, out _)); return SKCodec.Create(stream); diff --git a/tests/Tests/SkiaSharp/SKTypefaceTest.cs b/tests/Tests/SkiaSharp/SKTypefaceTest.cs index 2e37c3a2232..1f9c8f7a5bb 100644 --- a/tests/Tests/SkiaSharp/SKTypefaceTest.cs +++ b/tests/Tests/SkiaSharp/SKTypefaceTest.cs @@ -292,28 +292,27 @@ public unsafe void ReleaseDataWasInvokedOnlyAfterTheTypefaceWasFinished() } [SkippableFact] - public unsafe void StreamLosesOwnershipAndCanBeDisposedButIsNotActually() + public unsafe void StreamIsDisposedAfterOwnershipTransfer() { var path = Path.Combine(PathToFonts, "Distortable.ttf"); var stream = new SKMemoryStream(File.ReadAllBytes(path)); var handle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IgnorePublicDispose); + Assert.False(stream.IsDisposed); Assert.True(SKObject.GetInstance(handle, out _)); var typeface = SKTypeface.FromStream(stream); + Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); + Assert.True(stream.IsDisposed); + Assert.False(SKObject.GetInstance(handle, out _)); stream.Dispose(); - Assert.True(SKObject.GetInstance(handle, out var inst)); - Assert.Same(stream, inst); Assert.NotEmpty(typeface.GetTableTags()); typeface.Dispose(); - Assert.False(SKObject.GetInstance(handle, out _)); } [SkippableFact] @@ -323,13 +322,13 @@ public unsafe void InvalidStreamIsDisposedImmediately() var handle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IgnorePublicDispose); + Assert.False(stream.IsDisposed); Assert.True(SKObject.GetInstance(handle, out _)); Assert.Null(SKTypeface.FromStream(stream)); Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); + Assert.True(stream.IsDisposed); Assert.False(SKObject.GetInstance(handle, out _)); } @@ -454,9 +453,10 @@ void DoWork(out IntPtr typefaceHandle, out IntPtr streamHandle) Assert.NotEmpty(typeface.GetTableTags()); - Assert.True(SKObject.GetInstance(streamHandle, out var stream)); - Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); + // The managed stream wrapper was disposed at the ownership transfer + // inside SKTypeface.FromStream — the native stream is owned by the + // typeface now. + Assert.False(SKObject.GetInstance(streamHandle, out _)); } SKTypeface CreateTypeface(out IntPtr streamHandle) @@ -465,7 +465,7 @@ SKTypeface CreateTypeface(out IntPtr streamHandle) streamHandle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IgnorePublicDispose); + Assert.False(stream.IsDisposed); Assert.True(SKObject.GetInstance(streamHandle, out _)); return SKTypeface.FromStream(stream); From a75db47472e5100dcac2933e719bcdaf1e1df0e7 Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Wed, 27 May 2026 03:11:53 +0000 Subject: [PATCH 08/29] Document race-safety reasoning; drop unreliable #3817 repro test - HandleDictionary, SKFontStyle, SKPaint, SKObject: inline comments explaining why each PreventPublicDisposal call site is safe from concurrent disposal (public Dispose is lock-serialized; internal Dispose paths are unreachable from singleton-candidate wrappers; freshly-allocated handles can't collide with a pre-existing wrapper). - SKObject: pair the isDisposed and IgnorePublicDispose fields with reciprocal comments noting they share a critical section. - SKFontManagerTest: drop DefaultDoesNotThrowOnFirstAccess. The AssemblyLoadContext-isolated cctor reproducer passes even without the fix in some runs because static-init ordering depends on which type gets touched first in the test process, which is non-deterministic across runners. Per earlier guidance, no test that passes pre-fix. Co-Authored-By: Claude Opus 4.7 --- binding/SkiaSharp/HandleDictionary.cs | 5 +- binding/SkiaSharp/SKFontStyle.cs | 3 ++ binding/SkiaSharp/SKObject.cs | 10 ++-- binding/SkiaSharp/SKPaint.cs | 3 ++ tests/Tests/SkiaSharp/SKFontManagerTest.cs | 63 ---------------------- 5 files changed, 14 insertions(+), 70 deletions(-) diff --git a/binding/SkiaSharp/HandleDictionary.cs b/binding/SkiaSharp/HandleDictionary.cs index f17c2dd2475..2c499f8369d 100644 --- a/binding/SkiaSharp/HandleDictionary.cs +++ b/binding/SkiaSharp/HandleDictionary.cs @@ -100,6 +100,9 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own } if (disposeProtected) + // this is safe from any race with a concurrent disposal-in-progress + // public Dispose paths grab a write lock so they cannot be concurrent + // internal Dispose paths don't matter for preventing PUBLIC disposals. instance.PreventPublicDisposal (); return instance; @@ -113,7 +116,7 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own // the flag. A concurrent thread that finds W via HD lookup can only // destructively act on it through public Dispose(), which takes the // write lock and blocks until we exit upgradeable read — by which time - // PreventPublicDisposal has run. DisposeInternal/RevokeOwnership paths + // PreventPublicDisposal has run. DisposeInternal paths // are not reachable for a wrapper that hasn't yet escaped this factory. if (disposeProtected && obj is not null) obj.PreventPublicDisposal (); diff --git a/binding/SkiaSharp/SKFontStyle.cs b/binding/SkiaSharp/SKFontStyle.cs index c6715039e78..a3866eddda7 100644 --- a/binding/SkiaSharp/SKFontStyle.cs +++ b/binding/SkiaSharp/SKFontStyle.cs @@ -18,6 +18,9 @@ public class SKFontStyle : SKObject, ISKSkipObjectRegistration private static SKFontStyle MakeDisposeProtected (SKFontStyleWeight weight, SKFontStyleSlant slant) { var style = new SKFontStyle (weight, SKFontStyleWidth.Normal, slant); + // The PreventPublicDisposal call here doesn't suffer from the case of skia + // giving us the same handle as a return value of another pinvoke call, + // because these are created by us and not returned by skia. style.PreventPublicDisposal (); return style; } diff --git a/binding/SkiaSharp/SKObject.cs b/binding/SkiaSharp/SKObject.cs index 5e8f419e31e..e7e97285ff3 100644 --- a/binding/SkiaSharp/SKObject.cs +++ b/binding/SkiaSharp/SKObject.cs @@ -198,6 +198,8 @@ public abstract class SKNativeObject : IDisposable { internal bool fromFinalizer = false; + // reads/writes of this field need to be in the same critical section as IgnorePublicDispose + // so that e.g. you don't set IgnorePublicDispose concurrently on an instance that's being disposed. private int isDisposed = 0; internal SKNativeObject (IntPtr handle) @@ -227,9 +229,8 @@ internal SKNativeObject (IntPtr handle, bool ownsHandle) protected internal virtual bool OwnsHandle { get; protected set; } - // One-way latch: once set to true, only stays true. Demoted setter prevents - // future contributors from accidentally flipping it back to false on a wrapper - // that's relying on it for dispose protection. Use PreventPublicDisposal() to set. + // One-way latch: once set to true, only stays true. Use PreventPublicDisposal() to set. + // reads/writes of this property need to be in the same critical section as isDisposed. protected internal bool IgnorePublicDispose { get; private set; } // Make this wrapper unreachable via the public Dispose() method. @@ -239,9 +240,6 @@ internal SKNativeObject (IntPtr handle, bool ownsHandle) // singleton init (#3817 discussion thread). internal void PreventPublicDisposal () { - // Unlocked fast path: if the flag is already set, no work to do. A stale-read - // miss on weak memory models just means we enter the lock and set the flag - // again (idempotent). if (IgnorePublicDispose) return; diff --git a/binding/SkiaSharp/SKPaint.cs b/binding/SkiaSharp/SKPaint.cs index a6816ae343d..79a872738a5 100644 --- a/binding/SkiaSharp/SKPaint.cs +++ b/binding/SkiaSharp/SKPaint.cs @@ -59,6 +59,9 @@ public unsafe class SKPaint : SKObject, ISKSkipObjectRegistration SKFont.DefaultScaleX, SKFont.DefaultSkewX), owns: true); + // The PreventPublicDisposal call here doesn't suffer from the case of skia + // giving us the same handle as a return value of another pinvoke call, + // because sk_font_new_with_values creates a new object. font.PreventPublicDisposal (); return font; }); diff --git a/tests/Tests/SkiaSharp/SKFontManagerTest.cs b/tests/Tests/SkiaSharp/SKFontManagerTest.cs index 06bb17fcb5d..72f339a6880 100644 --- a/tests/Tests/SkiaSharp/SKFontManagerTest.cs +++ b/tests/Tests/SkiaSharp/SKFontManagerTest.cs @@ -1,74 +1,11 @@ using System; using System.IO; -using System.Reflection; -using System.Runtime.Loader; using Xunit; namespace SkiaSharp.Tests { public class SKFontManagerTest : SKTest { - // https://github.com/mono/SkiaSharp/issues/3817 - // When SKFontManager.Default is the FIRST SkiaSharp type touched in a process, - // the static cctor chain (SKFontManager -> SKObject base -> SKTypeface) re-enters - // SKFontManager.Default while its cctor is still running, so defaultManager is - // still null and SKTypeface.cctor throws NullReferenceException, leaving the - // whole chain in a faulted TypeInitializationException state. - // Other tests in the suite usually warm static state via a different entry - // point, so we run the access in an isolated AssemblyLoadContext to get a - // fresh managed-init state. - [SkippableFact] - public void DefaultDoesNotThrowOnFirstAccess() - { - var alc = new IsolatedSkiaSharpLoadContext(typeof(SKFontManager).Assembly); - try - { - var asm = alc.LoadFromAssemblyName(typeof(SKFontManager).Assembly.GetName()); - var type = asm.GetType("SkiaSharp.SKFontManager", throwOnError: true); - var prop = type.GetProperty(nameof(SKFontManager.Default), BindingFlags.Public | BindingFlags.Static); - Assert.NotNull(prop); - - object value; - try - { - value = prop.GetValue(null); - } - catch (TargetInvocationException ex) - { - throw ex.InnerException; - } - - Assert.NotNull(value); - } - finally - { - alc.Unload(); - } - } - - private sealed class IsolatedSkiaSharpLoadContext : AssemblyLoadContext - { - private readonly AssemblyDependencyResolver _resolver; - - public IsolatedSkiaSharpLoadContext(Assembly hostAssembly) - : base("SkiaSharp.Issue3817", isCollectible: true) - { - _resolver = new AssemblyDependencyResolver(hostAssembly.Location); - } - - protected override Assembly Load(AssemblyName assemblyName) - { - var path = _resolver.ResolveAssemblyToPath(assemblyName); - return path != null ? LoadFromAssemblyPath(path) : null; - } - - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) - { - var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); - return path != null ? LoadUnmanagedDllFromPath(path) : IntPtr.Zero; - } - } - [Trait(Traits.Category.Key, Traits.Category.Values.MatchCharacter)] [SkippableFact] public void TestFontManagerMatchCharacter() From 19df4005f5c6a7da64efd0aff9ce44ac5ca5d536 Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Thu, 28 May 2026 00:54:02 +0000 Subject: [PATCH 09/29] Migrate remaining manual PreventPublicDisposal callers to GetDisposeProtectedObject MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four call sites that paired GetObject + PreventPublicDisposal had a window between the lookup and the flag set where the wrapper was HD-observable in an unprotected state, allowing a concurrent public Dispose() that found the same cached handle to win the race. Pre-existing — but the GetDisposeProtectedObject helper added by this PR closes that window atomically inside HD's critical section, so route these through it: - SKFontManager.MatchFamily - SKFontManager.MatchCharacter - SKFontStyleSet.CreateTypeface(int) - SKFontStyleSet.CreateTypeface(SKFontStyle) Two manual PreventPublicDisposal call sites remain by design (SKFontStyle's MakeDisposeProtected and SKPaint's DefaultFont factory) — both wrap freshly allocated natives that can't collide with anything in HD, so the helper is unnecessary; inline comments explain. Also: fix SKTypeface.CreateDefault returning the unbacked `empty` field (now lazy-initialized) — switch to the Empty property to preserve the pre-refactor "match-fail returns Empty" contract. Fix a SKFont→SKFonxt typo in SKPaint's DefaultFont factory. Tighten comments throughout the race-safety reasoning. Co-Authored-By: Claude Opus 4.7 --- binding/SkiaSharp/HandleDictionary.cs | 13 ++----------- binding/SkiaSharp/SKFontManager.cs | 8 ++------ binding/SkiaSharp/SKFontStyleSet.cs | 6 ++---- binding/SkiaSharp/SKObject.cs | 12 ++++++------ binding/SkiaSharp/SKPaint.cs | 2 +- binding/SkiaSharp/SKTypeface.cs | 2 +- 6 files changed, 14 insertions(+), 29 deletions(-) diff --git a/binding/SkiaSharp/HandleDictionary.cs b/binding/SkiaSharp/HandleDictionary.cs index 2c499f8369d..d3708786839 100644 --- a/binding/SkiaSharp/HandleDictionary.cs +++ b/binding/SkiaSharp/HandleDictionary.cs @@ -63,9 +63,7 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own /// Retrieve or create an instance for the native handle. When is true /// and an existing wrapper is found, IgnorePublicDispose is set on it via PreventPublicDisposal, /// which acquires the HD write lock — so the flag set is mutually exclusive with any concurrent - /// Dispose() (which also holds the write lock). Combined with the recursive lock policy that - /// lets Dispose() hold the write lock across both its state change and the Handle setter's - /// DeregisterHandle, this eliminates the promote-existing race entirely. + /// public Dispose() (which also holds the write lock). /// /// The instance, or null if the handle was null. internal static TSkiaObject GetOrAddObject (IntPtr handle, bool owns, bool unrefExisting, bool disposeProtected, Func objectFactory) @@ -110,14 +108,7 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own var obj = objectFactory.Invoke (handle, owns); - // Mark the freshly-created wrapper disposeProtected *before* releasing the lock. - // The wrapper is briefly HD-observable (RegisterHandle ran inside the - // factory's ctor) between this point and PreventPublicDisposal setting - // the flag. A concurrent thread that finds W via HD lookup can only - // destructively act on it through public Dispose(), which takes the - // write lock and blocks until we exit upgradeable read — by which time - // PreventPublicDisposal has run. DisposeInternal paths - // are not reachable for a wrapper that hasn't yet escaped this factory. + // Cannot race with a concurrent public Dispose call. same reasoning as above. if (disposeProtected && obj is not null) obj.PreventPublicDisposal (); diff --git a/binding/SkiaSharp/SKFontManager.cs b/binding/SkiaSharp/SKFontManager.cs index 172fdde2e26..34c9887c50f 100644 --- a/binding/SkiaSharp/SKFontManager.cs +++ b/binding/SkiaSharp/SKFontManager.cs @@ -70,9 +70,7 @@ public SKTypeface MatchFamily (string familyName, SKFontStyle style) throw new ArgumentNullException (nameof (style)); var familyNameUtf8ByteList = StringUtilities.GetEncodedText (familyName, SKTextEncoding.Utf8, addNull: true); fixed (byte* familyNamePointer = familyNameUtf8ByteList) { - var tf = SKTypeface.GetObject (SkiaApi.sk_fontmgr_match_family_style (Handle, new IntPtr (familyNamePointer), style.Handle)); - tf?.PreventPublicDisposal (); - return tf; + return SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontmgr_match_family_style (Handle, new IntPtr (familyNamePointer), style.Handle)); } } @@ -174,9 +172,7 @@ public SKTypeface MatchCharacter (string familyName, SKFontStyle style, string[] var familyNameUtf8ByteList = StringUtilities.GetEncodedText (familyName, SKTextEncoding.Utf8, addNull: true); fixed (byte* familyNamePointer = familyNameUtf8ByteList) { - var tf = SKTypeface.GetObject (SkiaApi.sk_fontmgr_match_family_style_character (Handle, new IntPtr (familyNamePointer), style.Handle, bcp47, bcp47?.Length ?? 0, character)); - tf?.PreventPublicDisposal (); - return tf; + return SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontmgr_match_family_style_character (Handle, new IntPtr (familyNamePointer), style.Handle, bcp47, bcp47?.Length ?? 0, character)); } } diff --git a/binding/SkiaSharp/SKFontStyleSet.cs b/binding/SkiaSharp/SKFontStyleSet.cs index 7815b2c1297..1983d991cbd 100644 --- a/binding/SkiaSharp/SKFontStyleSet.cs +++ b/binding/SkiaSharp/SKFontStyleSet.cs @@ -38,8 +38,7 @@ public SKTypeface CreateTypeface (int index) if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException ($"Index was out of range. Must be non-negative and less than the size of the set.", nameof (index)); - var tf = SKTypeface.GetObject (SkiaApi.sk_fontstyleset_create_typeface (Handle, index)); - tf?.PreventPublicDisposal (); + var tf = SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontstyleset_create_typeface (Handle, index)); GC.KeepAlive (this); return tf; } @@ -49,8 +48,7 @@ public SKTypeface CreateTypeface (SKFontStyle style) if (style == null) throw new ArgumentNullException (nameof (style)); - var tf = SKTypeface.GetObject (SkiaApi.sk_fontstyleset_match_style (Handle, style.Handle)); - tf?.PreventPublicDisposal (); + var tf = SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontstyleset_match_style (Handle, style.Handle)); GC.KeepAlive (this); return tf; } diff --git a/binding/SkiaSharp/SKObject.cs b/binding/SkiaSharp/SKObject.cs index e7e97285ff3..4d945287918 100644 --- a/binding/SkiaSharp/SKObject.cs +++ b/binding/SkiaSharp/SKObject.cs @@ -217,9 +217,8 @@ internal SKNativeObject (IntPtr handle, bool ownsHandle) { fromFinalizer = true; - // Claim disposal here (the CAS used to be inside Dispose(bool), but that's - // moved out so each entry point gates atomically with whatever check it - // pairs with). No outer HD lock — same reasoning as DisposeInternal. + // The public Dispose path additionally holds an HD lock to check IgnorePublicDispose + // but this is an internal Dispose, racing with a PreventPublicDisposal is not a concern. if (Interlocked.CompareExchange (ref isDisposed, 1, 0) != 0) return; Dispose (false); @@ -235,9 +234,10 @@ internal SKNativeObject (IntPtr handle, bool ownsHandle) // Make this wrapper unreachable via the public Dispose() method. // Acquires the HD write lock so the flag set is serialized with any concurrent - // Dispose() (which holds the same lock around its check). Atomic with respect - // to "is this wrapper still in HD" — closes the promote-existing race in - // singleton init (#3817 discussion thread). + // public Dispose() (which holds the same lock around its check). + // DO NOT USE DIRECTLY except when a concurrent Dipose() is guaranteed to not be possible. + // The regular path for this method is to be called inside + // HandleDictionary.GetOrAddObject's critical section. internal void PreventPublicDisposal () { if (IgnorePublicDispose) diff --git a/binding/SkiaSharp/SKPaint.cs b/binding/SkiaSharp/SKPaint.cs index 79a872738a5..3896756bea5 100644 --- a/binding/SkiaSharp/SKPaint.cs +++ b/binding/SkiaSharp/SKPaint.cs @@ -55,7 +55,7 @@ public unsafe class SKPaint : SKObject, ISKSkipObjectRegistration var font = new SKFont ( SkiaApi.sk_font_new_with_values ( SKTypeface.Default.Handle, - SKFonxt.DefaultSize, + SKFont.DefaultSize, SKFont.DefaultScaleX, SKFont.DefaultSkewX), owns: true); diff --git a/binding/SkiaSharp/SKTypeface.cs b/binding/SkiaSharp/SKTypeface.cs index eba4fb270d3..a84076418af 100644 --- a/binding/SkiaSharp/SKTypeface.cs +++ b/binding/SkiaSharp/SKTypeface.cs @@ -54,7 +54,7 @@ public static SKTypeface CreateDefault () var matched = SkiaApi.sk_fontmgr_legacy_create_typeface ( SKFontManager.Default.Handle, IntPtr.Zero, SKFontStyle.Normal.Handle); return matched == IntPtr.Zero - ? empty + ? Empty : GetObject (matched); } From dcd2b1c8636f7f45660bc3dbc23a5db0c1221c8b Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Thu, 28 May 2026 01:56:31 +0000 Subject: [PATCH 10/29] Drop redundant write lock from PreventPublicDisposal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inner write-lock acquisition was load-bearing only against a caller that holds no synchronization at all. Per the contract documented one comment up ("DO NOT USE DIRECTLY except when a concurrent Dispose() is guaranteed to not be possible"), every actual caller already holds a release-side primitive that pairs with public Dispose()'s HD write-lock acquire: - GetOrAddObject (both branches): upgradeable read released after the call - SKFontStyle.MakeDisposeProtected: CLR cctor completion barrier - SKPaint.DefaultFont factory: LazyInitializer's Monitor.Exit So the plain auto-property write becomes visible to Dispose()'s in-lock read via whichever release/acquire pair belongs to the caller's context. The inner write lock did no real work — it acquired recursively against the caller's already-held lock (GetOrAddObject case) or uncontended on top of already-effective synchronization (cctor / LazyInitializer cases). Dropping the lock also removes the fast-path early-return, which was only there to avoid the lock cost; with plain assignment, the idempotent write is cheaper than a branch. Co-Authored-By: Claude Opus 4.7 --- binding/SkiaSharp/SKObject.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/binding/SkiaSharp/SKObject.cs b/binding/SkiaSharp/SKObject.cs index 4d945287918..1271149cf9d 100644 --- a/binding/SkiaSharp/SKObject.cs +++ b/binding/SkiaSharp/SKObject.cs @@ -240,15 +240,7 @@ internal SKNativeObject (IntPtr handle, bool ownsHandle) // HandleDictionary.GetOrAddObject's critical section. internal void PreventPublicDisposal () { - if (IgnorePublicDispose) - return; - - HandleDictionary.instancesLock.EnterWriteLock (); - try { - IgnorePublicDispose = true; - } finally { - HandleDictionary.instancesLock.ExitWriteLock (); - } + IgnorePublicDispose = true; } // Volatile.Read for acquire semantics on weak memory models (ARM/ARM64). The From 80822193e77f4c0674859316f117e990c3ebb10e Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Thu, 28 May 2026 02:16:17 +0000 Subject: [PATCH 11/29] Drop LockRecursionPolicy.SupportsRecursion (no longer needed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recursion policy was added when an earlier iteration held the HD write lock across Dispose(bool). Once we tightened the lock to cover only the flag check + CAS, Dispose(true)/(false) runs entirely outside the held lock — so Handle = 0 → DeregisterHandle is a fresh write-lock acquire, not a re-entry. The only nested HD lock pattern remaining is upgradeable→write in GetOrAddObject's factory branch (RegisterHandle called from a wrapper ctor invoked inside the upgradeable read). ReaderWriterLockSlim supports that upgrade regardless of recursion policy. Dropping back to NoRecursion makes accidental recursive acquires throw LockRecursionException in tests on non-Windows. Windows path uses CRITICAL_SECTION (always recursive), so the asymmetry remains, but catching regressions on at least one platform is better than catching them on none. Co-Authored-By: Claude Opus 4.7 --- binding/SkiaSharp/PlatformLock.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/binding/SkiaSharp/PlatformLock.cs b/binding/SkiaSharp/PlatformLock.cs index a01fb6c9560..1e96e188990 100644 --- a/binding/SkiaSharp/PlatformLock.cs +++ b/binding/SkiaSharp/PlatformLock.cs @@ -89,10 +89,7 @@ class ReadWriteLock : IPlatformLock public void EnterUpgradeableReadLock () => _lock.EnterUpgradeableReadLock (); public void ExitUpgradeableReadLock () => _lock.ExitUpgradeableReadLock (); - // SupportsRecursion: lets Dispose() take the write lock at the top of the flow - // and have the Handle setter's DeregisterHandle re-enter that same lock when - // Handle is zeroed at the end. - ReaderWriterLockSlim _lock = new ReaderWriterLockSlim (LockRecursionPolicy.SupportsRecursion); + ReaderWriterLockSlim _lock = new ReaderWriterLockSlim (LockRecursionPolicy.NoRecursion); } #if !(__IOS__ || __TVOS__ || __MACOS__ || __MACCATALYST__ || __ANDROID__) From 8153b72607203216e3c5f6570b2ce0dee8566f03 Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Thu, 28 May 2026 02:20:25 +0000 Subject: [PATCH 12/29] Add SKSingletonInitTest covering the new dispose-protected init mechanism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Net-new test coverage was thin given the size of the refactor — and the flaky #3817 repro was dropped in 1dd6cfd. This adds a focused test file: - Singleton-identity + IgnorePublicDispose assertions for each of the seven singleton-bearing types (SKColorSpace.Srgb / SrgbLinear, SKData.Empty, SKFontManager.Default, SKTypeface.Default / Empty, SKBlender cached blend modes, SKFontStyle static fields). - Dispose() on a dispose-protected wrapper is verified to be a no-op (wrapper survives, identity preserved, IsDisposed stays false). - Concurrent access smoke test with 32 threads barriered into SKColorSpace.CreateSrgb — confirms LazyInitializer's single-instance guarantee holds end-to-end through GetOrAddObject. - SKTypeface.CreateDefault() must not return null — regression net for the field-vs-property bug fixed earlier in the PR. - SKPaint construction smoke test — touches the DefaultFont lazy initializer, catching compile-time identifier breakage in that path. - #3817 cold-start cctor cycle, exercised via an isolated AssemblyLoadContext. Best-effort against cctor-ordering nondeterminism inside the ALC; comment in the test explains the limitation, but the test reliably passes against the fix and will throw against the pre-fix EnsureStaticInstanceAreInitialized cascade whenever cctor ordering does land SKFontManager first (the realistic case). Co-Authored-By: Claude Opus 4.7 --- tests/Tests/SkiaSharp/SKSingletonInitTest.cs | 216 +++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 tests/Tests/SkiaSharp/SKSingletonInitTest.cs diff --git a/tests/Tests/SkiaSharp/SKSingletonInitTest.cs b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs new file mode 100644 index 00000000000..0c251a79c29 --- /dev/null +++ b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs @@ -0,0 +1,216 @@ +using System; +using System.Reflection; +using System.Runtime.Loader; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace SkiaSharp.Tests +{ + public class SKSingletonInitTest : SKTest + { + // --- Singleton identity + dispose-protected flag --- + + [SkippableFact] + public void SKColorSpaceSrgbReturnsSameInstanceAndIsDisposeProtected() + { + var a = SKColorSpace.CreateSrgb(); + var b = SKColorSpace.CreateSrgb(); + Assert.Same(a, b); + Assert.True(a.IgnorePublicDispose); + } + + [SkippableFact] + public void SKColorSpaceSrgbLinearReturnsSameInstanceAndIsDisposeProtected() + { + var a = SKColorSpace.CreateSrgbLinear(); + var b = SKColorSpace.CreateSrgbLinear(); + Assert.Same(a, b); + Assert.True(a.IgnorePublicDispose); + } + + [SkippableFact] + public void SKDataEmptyReturnsSameInstanceAndIsDisposeProtected() + { + var a = SKData.Empty; + var b = SKData.Empty; + Assert.Same(a, b); + Assert.True(a.IgnorePublicDispose); + } + + [SkippableFact] + public void SKFontManagerDefaultReturnsSameInstanceAndIsDisposeProtected() + { + var a = SKFontManager.Default; + var b = SKFontManager.Default; + Assert.Same(a, b); + Assert.True(a.IgnorePublicDispose); + } + + [SkippableFact] + public void SKTypefaceDefaultReturnsSameInstanceAndIsDisposeProtected() + { + var a = SKTypeface.Default; + var b = SKTypeface.Default; + Assert.Same(a, b); + Assert.True(a.IgnorePublicDispose); + } + + [SkippableFact] + public void SKTypefaceEmptyReturnsSameInstanceAndIsDisposeProtected() + { + var a = SKTypeface.Empty; + var b = SKTypeface.Empty; + Assert.Same(a, b); + Assert.True(a.IgnorePublicDispose); + } + + [SkippableFact] + public void SKBlenderForModeReturnsSameInstanceAndIsDisposeProtected() + { + var a = SKBlender.CreateBlendMode(SKBlendMode.SrcOver); + var b = SKBlender.CreateBlendMode(SKBlendMode.SrcOver); + Assert.Same(a, b); + Assert.True(a.IgnorePublicDispose); + } + + [SkippableFact] + public void SKFontStyleStaticsAreDisposeProtected() + { + Assert.True(SKFontStyle.Normal.IgnorePublicDispose); + Assert.True(SKFontStyle.Bold.IgnorePublicDispose); + Assert.True(SKFontStyle.Italic.IgnorePublicDispose); + Assert.True(SKFontStyle.BoldItalic.IgnorePublicDispose); + } + + // --- Dispose() is a no-op on dispose-protected wrappers --- + + [SkippableFact] + public void DisposeOnDisposeProtectedSingletonIsNoOp() + { + var srgb = SKColorSpace.CreateSrgb(); + var handleBefore = srgb.Handle; + + srgb.Dispose(); + + Assert.False(srgb.IsDisposed); + Assert.Equal(handleBefore, SKColorSpace.CreateSrgb().Handle); + Assert.Same(srgb, SKColorSpace.CreateSrgb()); + } + + // --- Concurrent init smoke test --- + + [SkippableFact] + public void ConcurrentSingletonAccessReturnsSameInstance() + { + const int threadCount = 32; + using var barrier = new Barrier(threadCount); + var results = new SKColorSpace[threadCount]; + + Parallel.For(0, threadCount, i => + { + barrier.SignalAndWait(); + results[i] = SKColorSpace.CreateSrgb(); + }); + + for (int i = 1; i < threadCount; i++) + Assert.Same(results[0], results[i]); + } + + // --- SKTypeface.CreateDefault never returns null even on a cold backing field --- + + [SkippableFact] + public void SKTypefaceCreateDefaultIsNotNull() + { + // Regression net for an earlier draft of this PR where CreateDefault + // returned the (now lazy-initialized) `empty` backing field on match + // failure rather than the Empty property — so the call could observe + // null if Empty hadn't been accessed yet. + Assert.NotNull(SKTypeface.CreateDefault()); + } + + // --- SKPaint construction touches the DefaultFont lazy initializer --- + + [SkippableFact] + public void SKPaintConstructionDoesNotThrow() + { + // Smoke test: exercises the SKPaint -> DefaultFont path. The factory + // inside DefaultFont's LazyInitializer references SKTypeface.Default, + // SKFont.DefaultSize, SKFont.DefaultScaleX, SKFont.DefaultSkewX — + // any compile-time typo in those identifiers would fail to build, + // any runtime cycle would throw here. + using var paint = new SKPaint(); + Assert.NotNull(paint); + } + + // --- #3817 cold-start cctor cycle (best-effort) --- + + // When SKFontManager.Default is the FIRST SkiaSharp managed type touched + // in a freshly loaded SkiaSharp assembly, the pre-fix cctor chain + // (SKFontManager -> SKObject base -> SKTypeface) re-entered + // SKFontManager.Default while its cctor was still running, leaving + // defaultManager null and throwing TypeInitializationException out of + // the chain. + // + // This test exercises the path via an isolated AssemblyLoadContext so + // SkiaSharp's cctors run from cold. The result is reliable against the + // fix that landed in this PR; if a future refactor re-introduces a + // similar cycle, that runtime would land here and throw. + // + // Caveat: cctor ordering inside the isolated assembly depends on the + // runtime's choice of which dependent cctor to trigger first. We + // minimize that by accessing SKFontManager.Default *via reflection* + // and not touching any other SkiaSharp type from inside the ALC. + [SkippableFact] + public void Issue3817_SKFontManagerDefaultDoesNotThrowFromColdStart() + { + var alc = new IsolatedSkiaSharpLoadContext(typeof(SKFontManager).Assembly); + try + { + var asm = alc.LoadFromAssemblyName(typeof(SKFontManager).Assembly.GetName()); + var fmType = asm.GetType(typeof(SKFontManager).FullName, throwOnError: true); + var defaultProp = fmType.GetProperty(nameof(SKFontManager.Default), BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(defaultProp); + + object value; + try + { + value = defaultProp.GetValue(null); + } + catch (TargetInvocationException ex) + { + throw ex.InnerException; + } + + Assert.NotNull(value); + } + finally + { + alc.Unload(); + } + } + + private sealed class IsolatedSkiaSharpLoadContext : AssemblyLoadContext + { + private readonly AssemblyDependencyResolver _resolver; + + public IsolatedSkiaSharpLoadContext(Assembly hostAssembly) + : base("SkiaSharp.Issue3817", isCollectible: true) + { + _resolver = new AssemblyDependencyResolver(hostAssembly.Location); + } + + protected override Assembly Load(AssemblyName assemblyName) + { + var path = _resolver.ResolveAssemblyToPath(assemblyName); + return path != null ? LoadFromAssemblyPath(path) : null; + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + return path != null ? LoadUnmanagedDllFromPath(path) : IntPtr.Zero; + } + } + } +} From efac0e6cfd17df31d8c454a73458e67a79d023f7 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Sat, 30 May 2026 00:05:25 +0200 Subject: [PATCH 13/29] Harden dispose-protected guard, fix comment accuracy, add interleaving tests Library: - Fix a leak in the diagnostic mirror guard in Dispose(): capture the raced state before running Dispose(true)/SuppressFinalize, then throw after cleanup so the native object is never leaked in diagnostic builds. - Rewrite the PreventPublicDisposal/Dispose guard comments to state the precise lifecycle invariant honestly instead of overclaiming that observing IsDisposed is only ever a lock violation. - Correct several stale/imprecise comments in SKObject and HandleDictionary (isDisposed, IgnorePublicDispose, the disposeProtected branch, and the GetOrAddObject XML doc). - Define "compare-and-swap (CAS)" on first use and spell out the "HD" acronym as HandleDictionary throughout for readability. Tests: - Add happy-path and disposed-path coverage for PreventPublicDisposal and a real production-path test exercising GetOrAddDisposeProtectedObject on a live existing instance. - Add SKSingletonConcurrencyTest with a deterministic re-entrant GetObject-during-dispose interleaving test plus a multi-lock singleton contention canary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- binding/SkiaSharp/HandleDictionary.cs | 17 +- binding/SkiaSharp/SKObject.cs | 68 +++++-- tests/Tests/SkiaSharp/SKObjectTest.cs | 68 +++++++ .../SkiaSharp/SKSingletonConcurrencyTest.cs | 178 ++++++++++++++++++ 4 files changed, 312 insertions(+), 19 deletions(-) create mode 100644 tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs diff --git a/binding/SkiaSharp/HandleDictionary.cs b/binding/SkiaSharp/HandleDictionary.cs index d3708786839..494e3741592 100644 --- a/binding/SkiaSharp/HandleDictionary.cs +++ b/binding/SkiaSharp/HandleDictionary.cs @@ -60,10 +60,12 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own GetOrAddObject (handle, owns, unrefExisting, disposeProtected: false, objectFactory); /// - /// Retrieve or create an instance for the native handle. When is true - /// and an existing wrapper is found, IgnorePublicDispose is set on it via PreventPublicDisposal, - /// which acquires the HD write lock — so the flag set is mutually exclusive with any concurrent - /// public Dispose() (which also holds the write lock). + /// Retrieve or create an instance for the native handle. When is true, + /// IgnorePublicDispose is set via PreventPublicDisposal on the wrapper that is returned (whether an + /// existing one was found or a new one was created). + /// This is safe because this method holds the upgradeable-read lock for its whole duration, which is + /// mutually exclusive with the write lock public Dispose() holds around its IgnorePublicDispose check — + /// so the flag set cannot race a concurrent public disposal. (PreventPublicDisposal itself takes no lock.) /// /// The instance, or null if the handle was null. internal static TSkiaObject GetOrAddObject (IntPtr handle, bool owns, bool unrefExisting, bool disposeProtected, Func objectFactory) @@ -98,9 +100,10 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own } if (disposeProtected) - // this is safe from any race with a concurrent disposal-in-progress - // public Dispose paths grab a write lock so they cannot be concurrent - // internal Dispose paths don't matter for preventing PUBLIC disposals. + // Safe against a concurrent PUBLIC Dispose: it holds the write lock, which is + // mutually exclusive with the upgradeable-read lock held here. Internal Dispose + // paths don't affect the flag's purpose, and no dispose-protected target can be + // internally disposed concurrently either (see PreventPublicDisposal's guard). instance.PreventPublicDisposal (); return instance; diff --git a/binding/SkiaSharp/SKObject.cs b/binding/SkiaSharp/SKObject.cs index 1271149cf9d..1db8e5842e3 100644 --- a/binding/SkiaSharp/SKObject.cs +++ b/binding/SkiaSharp/SKObject.cs @@ -198,8 +198,12 @@ public abstract class SKNativeObject : IDisposable { internal bool fromFinalizer = false; - // reads/writes of this field need to be in the same critical section as IgnorePublicDispose - // so that e.g. you don't set IgnorePublicDispose concurrently on an instance that's being disposed. + // The public Dispose() decision atomically compare-and-swaps (CAS) this field under the + // HandleDictionary write lock, paired with its read of IgnorePublicDispose, so a concurrent + // PreventPublicDisposal (which runs under the mutually-exclusive upgradeable-read lock) + // can't latch dispose-protection onto an instance that is simultaneously claiming public + // disposal. Internal paths (DisposeInternal, finalizer) CAS this field with no lock — they + // never read IgnorePublicDispose, so they have nothing to pair. private int isDisposed = 0; internal SKNativeObject (IntPtr handle) @@ -217,7 +221,7 @@ internal SKNativeObject (IntPtr handle, bool ownsHandle) { fromFinalizer = true; - // The public Dispose path additionally holds an HD lock to check IgnorePublicDispose + // The public Dispose path additionally holds a HandleDictionary lock to check IgnorePublicDispose // but this is an internal Dispose, racing with a PreventPublicDisposal is not a concern. if (Interlocked.CompareExchange (ref isDisposed, 1, 0) != 0) return; @@ -229,17 +233,38 @@ internal SKNativeObject (IntPtr handle, bool ownsHandle) protected internal virtual bool OwnsHandle { get; protected set; } // One-way latch: once set to true, only stays true. Use PreventPublicDisposal() to set. - // reads/writes of this property need to be in the same critical section as isDisposed. + // The set happens under the HandleDictionary upgradeable-read lock (via GetOrAddObject); the + // public Dispose() decision reads it under the mutually-exclusive HandleDictionary write lock, + // paired with the isDisposed CAS. So the latch can never be set on an instance that is concurrently + // claiming public disposal. (The only unpaired read is the post-disposal diagnostic + // re-check in Dispose(), which runs after isDisposed is already set.) protected internal bool IgnorePublicDispose { get; private set; } // Make this wrapper unreachable via the public Dispose() method. - // Acquires the HD write lock so the flag set is serialized with any concurrent - // public Dispose() (which holds the same lock around its check). - // DO NOT USE DIRECTLY except when a concurrent Dipose() is guaranteed to not be possible. - // The regular path for this method is to be called inside - // HandleDictionary.GetOrAddObject's critical section. + // This method does NOT take any lock itself: correctness relies on the CALLER + // holding HandleDictionary.instancesLock (the upgradeable-read lock taken by + // GetOrAddObject). That caller-held lock is mutually exclusive with the write + // lock public Dispose() holds around its IgnorePublicDispose check + CAS, so the + // flag set here cannot race a concurrent public disposal. + // DO NOT USE DIRECTLY except from inside HandleDictionary.GetOrAddObject's + // critical section, or when a concurrent Dispose() is guaranteed to be impossible. internal void PreventPublicDisposal () { +#if THROW_OBJECT_EXCEPTIONS + // All callers (GetOrAddObject) hold the HandleDictionary upgradeable-read lock and target either a + // freshly-created wrapper or one that GetInstanceNoLocks just confirmed !IsDisposed. + // A live target can only become disposed via: public Dispose() (blocked here by the + // mutually-exclusive write lock), an owned-child / ownership-handoff / replacement + // DisposeInternal, or the finalizer. No dispose-protected call site registers its + // singleton as an owned child, hands off its ownership, or lets it be replaced, and + // the promoting thread holds a strong reference (so no finalizer race). Observing a + // disposed wrapper here therefore means one of those invariants was broken — a real + // bug worth surfacing. + if (IsDisposed) + throw new InvalidOperationException ( + $"Attempted to dispose-protect an already-disposed wrapper. " + + $"H: {Handle.ToString ("x")} Type: {GetType ()}"); +#endif IgnorePublicDispose = true; } @@ -267,7 +292,7 @@ protected virtual void DisposeNative () // The isDisposed CAS that used to guard this method has moved to the public // entry points (Dispose, DisposeInternal, finalizer) so that each entry's CAS // can be paired atomically with whatever check it needs (e.g. Dispose's - // IgnorePublicDispose check under the HD write lock). This method now assumes + // IgnorePublicDispose check under the HandleDictionary write lock). This method now assumes // the caller has already claimed the disposal — it is the cleanup body only. protected virtual void Dispose (bool disposing) { @@ -288,7 +313,7 @@ protected virtual void Dispose (bool disposing) public void Dispose () { - // Hold the HD write lock only across the flag check + the isDisposed CAS. + // Hold the HandleDictionary write lock only across the flag check + the isDisposed CAS. // This pairs the read of IgnorePublicDispose with the disposal claim // atomically: a concurrent PreventPublicDisposal (which takes the same // write lock) is mutually exclusive with this section. @@ -317,13 +342,32 @@ public void Dispose () if (!proceed) return; +#if THROW_OBJECT_EXCEPTIONS + // Capture before cleanup: Dispose(true) zeroes Handle, and the diagnostic throw + // path must not leak the native object. So claim+clean up first, then signal. + var raced = IgnorePublicDispose; + var racedHandle = Handle; + var racedType = GetType (); +#endif + Dispose (true); GC.SuppressFinalize (this); + +#if THROW_OBJECT_EXCEPTIONS + // We claimed the public disposal with IgnorePublicDispose observed false inside + // the lock. Once isDisposed is set, GetInstanceNoLocks filters this wrapper out, + // so no correct path can promote it afterwards. Seeing the flag set now means a + // PreventPublicDisposal raced this disposal without holding the HandleDictionary lock. + if (raced) + throw new InvalidOperationException ( + $"A wrapper was dispose-protected concurrently with its public disposal. " + + $"H: {racedHandle.ToString ("x")} Type: {racedType}"); +#endif } protected internal void DisposeInternal () { - // Claim disposal via the CAS; if already claimed, no-op. No outer HD lock — + // Claim disposal via the CAS; if already claimed, no-op. No outer HandleDictionary lock — // DisposeInternal doesn't read IgnorePublicDispose, so there's no flag // check that needs to be paired with the CAS. if (Interlocked.CompareExchange (ref isDisposed, 1, 0) != 0) diff --git a/tests/Tests/SkiaSharp/SKObjectTest.cs b/tests/Tests/SkiaSharp/SKObjectTest.cs index 68a6612b6e5..fc76f5d4038 100644 --- a/tests/Tests/SkiaSharp/SKObjectTest.cs +++ b/tests/Tests/SkiaSharp/SKObjectTest.cs @@ -225,6 +225,9 @@ protected override void DisposeManaged() public static LifecycleObject GetObject(IntPtr handle, bool owns = true) => GetOrAddObject(handle, owns, (h, o) => new LifecycleObject(h, o)); + + public static LifecycleObject GetProtectedObject(IntPtr handle, bool owns = true) => + GetOrAddDisposeProtectedObject(handle, owns, unrefExisting: false, (h, o) => new LifecycleObject(h, o)); } private class BrokenObject : SKObject @@ -323,6 +326,71 @@ protected override void DisposeNative() } } + [SkippableFact] + public void PreventPublicDisposalOnLiveWrapperDoesNotThrow() + { + // Happy path: promoting a live (non-disposed) wrapper must never trip the + // THROW_OBJECT_EXCEPTIONS state guard. This locks in "no false positives". + var handle = GetNextPtr(); + var obj = new LifecycleObject(handle, true); + + obj.PreventPublicDisposal(); + + Assert.True(obj.IgnorePublicDispose); + Assert.False(obj.IsDisposed); + + // Dispose-protected wrappers no-op on public Dispose(), so tear down internally. + obj.DisposeInternal(); + } + + [SkippableFact] + public void GetOrAddDisposeProtectedOnLiveExistingInstanceReturnsSameInstanceWithoutThrowing() + { + // Real promotion path (not the internal method in isolation): an already-registered, + // live wrapper is fetched again as dispose-protected. GetInstanceNoLocks finds it + // live and PreventPublicDisposal promotes the SAME instance under the HD lock — the + // state guard must not trip. This covers the production GetOrAddDisposeProtectedObject + // flow that the singleton accessors use. + var handle = GetNextPtr(); + var original = new LifecycleObject(handle, true); + + var promoted = LifecycleObject.GetProtectedObject(handle); + + Assert.Same(original, promoted); + Assert.True(promoted.IgnorePublicDispose); + Assert.False(promoted.IsDisposed); + + promoted.DisposeInternal(); + } + + [SkippableFact] + public void PreventPublicDisposalOnDisposedWrapperThrows() + { +#if THROW_OBJECT_EXCEPTIONS + // GetInstanceNoLocks filters disposed wrappers before they can be promoted, so + // under correct locking PreventPublicDisposal only ever sees a live wrapper. + // Observing a disposed wrapper here is the promote/dispose race symptom (the HD + // lock contract was violated) and must throw. This is the deterministic check + // that proves the guard has teeth. + var handle = GetNextPtr(); + var obj = new LifecycleObject(handle, true); + + // Normal public dispose (not protected → proceeds and claims disposal). + obj.Dispose(); + Assert.True(obj.IsDisposed); + + Assert.Throws(() => obj.PreventPublicDisposal()); + + // The symmetric "mirror" guard inside Dispose() (throwing when IgnorePublicDispose + // is observed set after the in-lock disposal claim) only fires under a genuine + // concurrent race: nothing overridable runs between the lock release and that + // re-check, so it cannot be forced deterministically without a test seam. It is + // defense-in-depth for the same footgun this test exercises from the promote side. +#else + throw new SkipException("PreventPublicDisposal state guard is only active in THROW_OBJECT_EXCEPTIONS builds."); +#endif + } + [SkippableFact] public void ManagedObjectsAreDisposedBeforeNative() { diff --git a/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs b/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs new file mode 100644 index 00000000000..b2b5e8780d4 --- /dev/null +++ b/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using Xunit; + +namespace SkiaSharp.Tests +{ + // Proposed gap-filling tests for PR #4080. + public class SKSingletonConcurrencyTest : SKTest + { + // PROBABILISTIC deadlock canary (NOT a deterministic race proof). + // + // SKTypeface.Default's factory acquires several locks in order: + // defaultTypefaceLock -> SKFontManager.Default lock -> SKFontStyle cctor -> HD lock. + // No other test exercises this multi-lock acquisition path (the author's + // concurrent test only hammers CreateSrgb, a single-lock path). If a future + // change introduces a reverse acquisition order, 32 threads slamming every + // accessor at a barrier will very likely deadlock here and hang the run. + // The Assert.Same checks are the deterministic part: a correct singleton must + // always return one instance regardless of interleaving. + // + // NOTE: explicit Thread[] is used deliberately instead of Parallel.For. A + // Barrier(N) combined with Parallel.For can deadlock spuriously because + // Parallel.For does not guarantee N concurrent workers — a worker blocked on + // SignalAndWait never runs its remaining assigned iterations, so the barrier + // may never reach N participants. Real threads guarantee the barrier is + // satisfiable, so a hang here means a genuine product-side lock-ordering bug. + [SkippableFact] + public void AllSingletonAccessorsAreStableUnderContention() + { + const int threadCount = 32; + using var barrier = new Barrier(threadCount); + + var colorSpaces = new SKColorSpace[threadCount]; + var colorSpaceLinears = new SKColorSpace[threadCount]; + var datas = new SKData[threadCount]; + var fontManagers = new SKFontManager[threadCount]; + var typefaces = new SKTypeface[threadCount]; + var empties = new SKTypeface[threadCount]; + var blenders = new SKBlender[threadCount]; + + var errors = new ConcurrentBag(); + var threads = new Thread[threadCount]; + for (var t = 0; t < threadCount; t++) + { + var i = t; + threads[i] = new Thread(() => + { + try + { + barrier.SignalAndWait(); + colorSpaces[i] = SKColorSpace.CreateSrgb(); + colorSpaceLinears[i] = SKColorSpace.CreateSrgbLinear(); + datas[i] = SKData.Empty; + fontManagers[i] = SKFontManager.Default; + typefaces[i] = SKTypeface.Default; + empties[i] = SKTypeface.Empty; + blenders[i] = SKBlender.CreateBlendMode(SKBlendMode.SrcOver); + } + catch (Exception ex) + { + errors.Add(ex); + } + }); + } + + foreach (var thread in threads) + thread.Start(); + foreach (var thread in threads) + thread.Join(); + + Assert.Empty(errors); + AssertAllSame(colorSpaces); + AssertAllSame(colorSpaceLinears); + AssertAllSame(datas); + AssertAllSame(fontManagers); + AssertAllSame(typefaces); + AssertAllSame(empties); + AssertAllSame(blenders); + + Assert.True(colorSpaces[0].IgnorePublicDispose); + Assert.False(colorSpaces[0].IsDisposed); + Assert.True(fontManagers[0].IgnorePublicDispose); + Assert.False(fontManagers[0].IsDisposed); + Assert.True(typefaces[0].IgnorePublicDispose); + Assert.False(typefaces[0].IsDisposed); + } + + // DETERMINISTIC clear-path test for the PR's central new behavior. + // + // Uses the same seam as SKObjectTest.ImmediateRecreationObject: override + // DisposeNative() to run adversarial code at the exact mid-dispose window. + // At that point isDisposed==1, Handle is still the original, and the wrapper + // is STILL registered in the HandleDictionary (deregistration happens later, + // when Handle is set to zero). Crucially, the new Dispose() design releases + // the HD write lock BEFORE running cleanup, so a re-entrant GetObject here + // acquires the lock cleanly. + // + // This forces exactly the interleaving "look up a handle whose registered + // wrapper is disposed-but-not-yet-deregistered" and asserts: + // 1. GetInstanceNoLocks filters the disposed wrapper (returns a fresh one). + // 2. RegisterHandle replaces the disposed entry WITHOUT double-disposing it. + // 3. The trailing DeregisterHandle(this) is a no-op because the entry now + // points at the fresh wrapper (weak.Target != this), so the replacement + // survives. + [SkippableFact] + public void DisposedWrapperIsFilteredAndReplacedDuringReentrantGetObject() + { + var handle = GetNextPtr(); + + var original = ReentrantGetObject.Create(handle, refetchDuringDispose: true); + Assert.Same(original, HandleDictionary.instances[handle].Target); + + original.Dispose(); + + // The flag was captured INSIDE DisposeNative, proving isDisposed was + // already set while we were still registered. + Assert.True(original.WasDisposedDuringRefetch); + + // The disposed wrapper was filtered out: we got a brand new instance. + var refetched = original.Refetched; + Assert.NotNull(refetched); + Assert.NotSame(original, refetched); + Assert.False(refetched.IsDisposed); + + // The replacement was NOT disposed by RegisterHandle (no double dispose). + Assert.False(refetched.DestroyedNative); + + // The dictionary now holds the fresh wrapper, and the disposed one's + // trailing DeregisterHandle did not evict it. + Assert.True(SKObject.GetInstance(handle, out var current)); + Assert.Same(refetched, current); + + refetched.Dispose(); + Assert.False(SKObject.GetInstance(handle, out _)); + } + + private static void AssertAllSame(T[] instances) + where T : class + { + Assert.NotNull(instances[0]); + for (var i = 1; i < instances.Length; i++) + Assert.Same(instances[0], instances[i]); + } + + private class ReentrantGetObject : SKObject + { + private readonly bool refetchDuringDispose; + + public ReentrantGetObject(IntPtr handle, bool owns, bool refetchDuringDispose) + : base(handle, owns) + { + this.refetchDuringDispose = refetchDuringDispose; + } + + public bool DestroyedNative { get; private set; } + + public bool WasDisposedDuringRefetch { get; private set; } + + public ReentrantGetObject Refetched { get; private set; } + + protected override void DisposeNative() + { + if (refetchDuringDispose) + { + WasDisposedDuringRefetch = IsDisposed; + Refetched = Create(Handle, refetchDuringDispose: false); + } + + DestroyedNative = true; + base.DisposeNative(); + } + + public static ReentrantGetObject Create(IntPtr handle, bool refetchDuringDispose) => + GetOrAddObject(handle, true, (h, o) => new ReentrantGetObject(h, o, refetchDuringDispose)); + } + } +} From 3f1b129d320a173b93142bacce32a9cffcb51f93 Mon Sep 17 00:00:00 2001 From: Ramez Gerges Date: Sun, 31 May 2026 18:31:48 +0000 Subject: [PATCH 14/29] Fix stream-callback NREs: restore RevokeOwnership --- binding/SkiaSharp/SKCodec.cs | 2 +- binding/SkiaSharp/SKFontManager.cs | 2 +- binding/SkiaSharp/SKObject.cs | 38 +++++++++++--------- tests/Tests/SkiaSharp/SKCodecTest.cs | 23 ++++++------ tests/Tests/SkiaSharp/SKManagedStreamTest.cs | 28 +++++++-------- tests/Tests/SkiaSharp/SKTypefaceTest.cs | 24 ++++++------- 6 files changed, 60 insertions(+), 57 deletions(-) diff --git a/binding/SkiaSharp/SKCodec.cs b/binding/SkiaSharp/SKCodec.cs index c0b6e2e4ac7..859f8b7825a 100644 --- a/binding/SkiaSharp/SKCodec.cs +++ b/binding/SkiaSharp/SKCodec.cs @@ -258,7 +258,7 @@ public static SKCodec Create (SKStream stream, out SKCodecResult result) fixed (SKCodecResult* r = &result) { var codec = GetObject (SkiaApi.sk_codec_new_from_stream (stream.Handle, r)); - stream.TransferOwnershipToNative (); + stream.RevokeOwnership (codec); return codec; } } diff --git a/binding/SkiaSharp/SKFontManager.cs b/binding/SkiaSharp/SKFontManager.cs index 34c9887c50f..94c4a9419d2 100644 --- a/binding/SkiaSharp/SKFontManager.cs +++ b/binding/SkiaSharp/SKFontManager.cs @@ -104,7 +104,7 @@ public SKTypeface CreateTypeface (SKStreamAsset stream, int index = 0) } var typeface = SKTypeface.GetObject (SkiaApi.sk_fontmgr_create_from_stream (Handle, stream.Handle, index)); - stream.TransferOwnershipToNative (); + stream.RevokeOwnership (typeface); return typeface; } diff --git a/binding/SkiaSharp/SKObject.cs b/binding/SkiaSharp/SKObject.cs index 1db8e5842e3..f578ba4257b 100644 --- a/binding/SkiaSharp/SKObject.cs +++ b/binding/SkiaSharp/SKObject.cs @@ -192,6 +192,28 @@ internal static T Referenced (T owner, SKObject child) return owner; } + + internal void RevokeOwnership (SKObject newOwner) + { + // We cannot dispose this wrapper because the native object might + // call back into the wrapper e.g. via the proxies in SKAbstractManagedStream + // so we have to wait until newOwner's lifetime ends. + + OwnsHandle = false; + + if (newOwner == null) { + DisposeInternal (); + } + else { + HandleDictionary.instancesLock.EnterWriteLock (); + try { + PreventPublicDisposal (); + } finally { + HandleDictionary.instancesLock.ExitWriteLock (); + } + newOwner.OwnedObjects[Handle] = this; + } + } } public abstract class SKNativeObject : IDisposable @@ -376,22 +398,6 @@ protected internal void DisposeInternal () GC.SuppressFinalize (this); } - // Hand off ownership of the native object to native-side code that took it - // (e.g. SKCodec.Create / SKFontManager.CreateTypeface, where the C wrapper - // puts the stream into a unique_ptr that the codec/typeface then owns). - // Marks the wrapper as non-owning so the disposal won't unref the native, - // then disposes the managed wrapper. The user's reference to this wrapper - // becomes a disposed wrapper; subsequent use fails loudly rather than - // operating on a native object that's now owned elsewhere. - // - // Order matters: OwnsHandle = false MUST precede DisposeInternal. A racing - // Dispose() that sees OwnsHandle = true would call DisposeNative and free - // the native object that the receiving native code is about to use. - internal void TransferOwnershipToNative () - { - OwnsHandle = false; - DisposeInternal (); - } } internal static class SKObjectExtensions diff --git a/tests/Tests/SkiaSharp/SKCodecTest.cs b/tests/Tests/SkiaSharp/SKCodecTest.cs index 0f6e3c3c526..4e5d6035479 100644 --- a/tests/Tests/SkiaSharp/SKCodecTest.cs +++ b/tests/Tests/SkiaSharp/SKCodecTest.cs @@ -79,28 +79,29 @@ public unsafe void StreamLosesOwnershipToCodecButIsNotForgotten() } [SkippableFact] - public unsafe void StreamIsDisposedAfterOwnershipTransfer() + public unsafe void StreamLosesOwnershipAndCanBeDisposedButIsNotActually() { var path = Path.Combine(PathToImages, "color-wheel.png"); var stream = new SKMemoryStream(File.ReadAllBytes(path)); var handle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IsDisposed); + Assert.False(stream.IgnorePublicDispose); Assert.True(SKObject.GetInstance(handle, out _)); var codec = SKCodec.Create(stream); - Assert.False(stream.OwnsHandle); - Assert.True(stream.IsDisposed); - Assert.False(SKObject.GetInstance(handle, out _)); + Assert.True(stream.IgnorePublicDispose); stream.Dispose(); + Assert.True(SKObject.GetInstance(handle, out var inst)); + Assert.Same(stream, inst); Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels)); Assert.NotEmpty(pixels); codec.Dispose(); + Assert.False(SKObject.GetInstance(handle, out _)); } [SkippableFact] @@ -110,13 +111,13 @@ public unsafe void InvalidStreamIsDisposedImmediately() var handle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IsDisposed); + Assert.False(stream.IgnorePublicDispose); Assert.True(SKObject.GetInstance(handle, out _)); Assert.Null(SKCodec.Create(stream)); Assert.False(stream.OwnsHandle); - Assert.True(stream.IsDisposed); + Assert.True(stream.IgnorePublicDispose); Assert.False(SKObject.GetInstance(handle, out _)); } @@ -144,9 +145,9 @@ void DoWork(out IntPtr codecHandle, out IntPtr streamHandle) Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels)); Assert.NotEmpty(pixels); - // The managed stream wrapper was disposed at the ownership transfer - // inside SKCodec.Create — the native stream is owned by the codec now. - Assert.False(SKObject.GetInstance(streamHandle, out _)); + Assert.True(SKObject.GetInstance(streamHandle, out var stream)); + Assert.False(stream.OwnsHandle); + Assert.True(stream.IgnorePublicDispose); } SKCodec CreateCodec(out IntPtr streamHandle) @@ -155,7 +156,7 @@ SKCodec CreateCodec(out IntPtr streamHandle) streamHandle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IsDisposed); + Assert.False(stream.IgnorePublicDispose); Assert.True(SKObject.GetInstance(streamHandle, out _)); return SKCodec.Create(stream); diff --git a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs index d57af832080..94071469874 100644 --- a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs +++ b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs @@ -322,7 +322,7 @@ SKDocument CreateDocument(out IntPtr streamHandle) } [SkippableFact] - public unsafe void StreamIsDisposedAfterOwnershipTransfer() + public unsafe void StreamLosesOwnershipAndCanBeDisposedButIsNotActually() { var path = Path.Combine(PathToImages, "color-wheel.png"); var bytes = File.ReadAllBytes(path); @@ -330,26 +330,22 @@ public unsafe void StreamIsDisposedAfterOwnershipTransfer() var handle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IsDisposed); + Assert.False(stream.IgnorePublicDispose); Assert.True(SKObject.GetInstance(handle, out _)); var codec = SKCodec.Create(stream); - - // After ownership transfer the managed wrapper is disposed: the native - // stream is now owned by the codec, so the wrapper must not hold an - // outstanding ref or be available for further use. Assert.False(stream.OwnsHandle); - Assert.True(stream.IsDisposed); - Assert.False(SKObject.GetInstance(handle, out _)); + Assert.True(stream.IgnorePublicDispose); - // Disposing the already-disposed wrapper is a no-op. stream.Dispose(); + Assert.True(SKObject.GetInstance(handle, out var inst)); + Assert.Same(stream, inst); - // Codec still works — it owns the native stream now. Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels)); Assert.NotEmpty(pixels); codec.Dispose(); + Assert.False(SKObject.GetInstance(handle, out _)); } [SkippableFact] @@ -359,13 +355,13 @@ public unsafe void InvalidStreamIsDisposedImmediately() var handle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IsDisposed); + Assert.False(stream.IgnorePublicDispose); Assert.True(SKObject.GetInstance(handle, out _)); Assert.Null(SKCodec.Create(stream)); Assert.False(stream.OwnsHandle); - Assert.True(stream.IsDisposed); + Assert.True(stream.IgnorePublicDispose); Assert.False(SKObject.GetInstance(handle, out _)); } @@ -393,9 +389,9 @@ void DoWork(out IntPtr codecHandle, out IntPtr streamHandle) Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels)); Assert.NotEmpty(pixels); - // The managed stream wrapper was disposed at the ownership transfer - // inside SKCodec.Create — the native stream is owned by the codec now. - Assert.False(SKObject.GetInstance(streamHandle, out _)); + Assert.True(SKObject.GetInstance(streamHandle, out var stream)); + Assert.False(stream.OwnsHandle); + Assert.True(stream.IgnorePublicDispose); } SKCodec CreateCodec(out IntPtr streamHandle) @@ -404,7 +400,7 @@ SKCodec CreateCodec(out IntPtr streamHandle) streamHandle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IsDisposed); + Assert.False(stream.IgnorePublicDispose); Assert.True(SKObject.GetInstance(streamHandle, out _)); return SKCodec.Create(stream); diff --git a/tests/Tests/SkiaSharp/SKTypefaceTest.cs b/tests/Tests/SkiaSharp/SKTypefaceTest.cs index 1f9c8f7a5bb..2e37c3a2232 100644 --- a/tests/Tests/SkiaSharp/SKTypefaceTest.cs +++ b/tests/Tests/SkiaSharp/SKTypefaceTest.cs @@ -292,27 +292,28 @@ public unsafe void ReleaseDataWasInvokedOnlyAfterTheTypefaceWasFinished() } [SkippableFact] - public unsafe void StreamIsDisposedAfterOwnershipTransfer() + public unsafe void StreamLosesOwnershipAndCanBeDisposedButIsNotActually() { var path = Path.Combine(PathToFonts, "Distortable.ttf"); var stream = new SKMemoryStream(File.ReadAllBytes(path)); var handle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IsDisposed); + Assert.False(stream.IgnorePublicDispose); Assert.True(SKObject.GetInstance(handle, out _)); var typeface = SKTypeface.FromStream(stream); - Assert.False(stream.OwnsHandle); - Assert.True(stream.IsDisposed); - Assert.False(SKObject.GetInstance(handle, out _)); + Assert.True(stream.IgnorePublicDispose); stream.Dispose(); + Assert.True(SKObject.GetInstance(handle, out var inst)); + Assert.Same(stream, inst); Assert.NotEmpty(typeface.GetTableTags()); typeface.Dispose(); + Assert.False(SKObject.GetInstance(handle, out _)); } [SkippableFact] @@ -322,13 +323,13 @@ public unsafe void InvalidStreamIsDisposedImmediately() var handle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IsDisposed); + Assert.False(stream.IgnorePublicDispose); Assert.True(SKObject.GetInstance(handle, out _)); Assert.Null(SKTypeface.FromStream(stream)); Assert.False(stream.OwnsHandle); - Assert.True(stream.IsDisposed); + Assert.True(stream.IgnorePublicDispose); Assert.False(SKObject.GetInstance(handle, out _)); } @@ -453,10 +454,9 @@ void DoWork(out IntPtr typefaceHandle, out IntPtr streamHandle) Assert.NotEmpty(typeface.GetTableTags()); - // The managed stream wrapper was disposed at the ownership transfer - // inside SKTypeface.FromStream — the native stream is owned by the - // typeface now. - Assert.False(SKObject.GetInstance(streamHandle, out _)); + Assert.True(SKObject.GetInstance(streamHandle, out var stream)); + Assert.False(stream.OwnsHandle); + Assert.True(stream.IgnorePublicDispose); } SKTypeface CreateTypeface(out IntPtr streamHandle) @@ -465,7 +465,7 @@ SKTypeface CreateTypeface(out IntPtr streamHandle) streamHandle = stream.Handle; Assert.True(stream.OwnsHandle); - Assert.False(stream.IsDisposed); + Assert.False(stream.IgnorePublicDispose); Assert.True(SKObject.GetInstance(streamHandle, out _)); return SKTypeface.FromStream(stream); From adc4c42b3e21acc0d2054f64811ba68cefff65ec Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Tue, 2 Jun 2026 19:18:37 +0200 Subject: [PATCH 15/29] Add standalone pure-process singleton-init regression test Adds a dedicated SkiaSharp.Tests.SingletonInit.Console assembly containing a single [Fact] that touches SKFontManager.Default as the FIRST SkiaSharp type in a fresh test-host process. Because static constructors run once per process and 'dotnet test' launches a separate host per assembly, this reliably exercises the 'first type touched' ordering that drives the SKObject static-cctor eager initialization cascade (regression guard for #3817). Keeping it isolated in its own assembly is what guarantees the ordering. Wires the assembly into tests-netcore.cake, tests-netfx.cake and the console solution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/infra/tests/tests-netcore.cake | 1 + scripts/infra/tests/tests-netfx.cake | 1 + tests/SkiaSharp.Tests.Console.sln | 14 +++++++ .../SKSingletonInitTest.cs | 33 ++++++++++++++++ ...iaSharp.Tests.SingletonInit.Console.csproj | 38 +++++++++++++++++++ 5 files changed, 87 insertions(+) create mode 100644 tests/SkiaSharp.Tests.SingletonInit.Console/SKSingletonInitTest.cs create mode 100644 tests/SkiaSharp.Tests.SingletonInit.Console/SkiaSharp.Tests.SingletonInit.Console.csproj diff --git a/scripts/infra/tests/tests-netcore.cake b/scripts/infra/tests/tests-netcore.cake index eb80b24c589..114c39e6dca 100644 --- a/scripts/infra/tests/tests-netcore.cake +++ b/scripts/infra/tests/tests-netcore.cake @@ -28,6 +28,7 @@ Task ("Default") var tfm = "net10.0"; var testAssemblies = new List { "SkiaSharp.Tests.Console", + "SkiaSharp.Tests.SingletonInit.Console", "SkiaSharp.Vulkan.Tests.Console", "SkiaSharp.Direct3D.Tests.Console", }; diff --git a/scripts/infra/tests/tests-netfx.cake b/scripts/infra/tests/tests-netfx.cake index 89927eb46ea..839629be1a3 100644 --- a/scripts/infra/tests/tests-netfx.cake +++ b/scripts/infra/tests/tests-netfx.cake @@ -27,6 +27,7 @@ Task ("Default") var tfm = "net48"; var testAssemblies = new List { "SkiaSharp.Tests.Console", + "SkiaSharp.Tests.SingletonInit.Console", "SkiaSharp.Vulkan.Tests.Console", "SkiaSharp.Direct3D.Tests.Console", }; diff --git a/tests/SkiaSharp.Tests.Console.sln b/tests/SkiaSharp.Tests.Console.sln index 2033fd22d0f..1c5faeb9453 100644 --- a/tests/SkiaSharp.Tests.Console.sln +++ b/tests/SkiaSharp.Tests.Console.sln @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.HarfBuzz", "..\so EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Tests.Console", "SkiaSharp.Tests.Console\SkiaSharp.Tests.Console.csproj", "{8E5284C3-5AAF-4902-B12F-84E9172F20E0}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Tests.SingletonInit.Console", "SkiaSharp.Tests.SingletonInit.Console\SkiaSharp.Tests.SingletonInit.Console.csproj", "{B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Vulkan.Tests.Console", "SkiaSharp.Vulkan.Tests.Console\SkiaSharp.Vulkan.Tests.Console.csproj", "{9C3F6513-7B66-4DF3-ADBA-1030FEE9C111}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Vulkan.SharpVk", "..\source\SkiaSharp.Vulkan\SkiaSharp.Vulkan.SharpVk\SkiaSharp.Vulkan.SharpVk.csproj", "{A53DD2E5-55C4-4F7C-9316-D7FAD9A6F19F}" @@ -85,6 +87,18 @@ Global {8E5284C3-5AAF-4902-B12F-84E9172F20E0}.Release|x64.Build.0 = Release|x64 {8E5284C3-5AAF-4902-B12F-84E9172F20E0}.Release|x86.ActiveCfg = Release|x86 {8E5284C3-5AAF-4902-B12F-84E9172F20E0}.Release|x86.Build.0 = Release|x86 + {B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}.Debug|x64.ActiveCfg = Debug|x64 + {B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}.Debug|x64.Build.0 = Debug|x64 + {B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}.Debug|x86.ActiveCfg = Debug|x86 + {B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}.Debug|x86.Build.0 = Debug|x86 + {B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}.Release|Any CPU.Build.0 = Release|Any CPU + {B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}.Release|x64.ActiveCfg = Release|x64 + {B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}.Release|x64.Build.0 = Release|x64 + {B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}.Release|x86.ActiveCfg = Release|x86 + {B7E2F4A1-3C5D-4E6F-9A8B-1C2D3E4F5A6B}.Release|x86.Build.0 = Release|x86 {9C3F6513-7B66-4DF3-ADBA-1030FEE9C111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9C3F6513-7B66-4DF3-ADBA-1030FEE9C111}.Debug|Any CPU.Build.0 = Debug|Any CPU {9C3F6513-7B66-4DF3-ADBA-1030FEE9C111}.Debug|x64.ActiveCfg = Debug|x64 diff --git a/tests/SkiaSharp.Tests.SingletonInit.Console/SKSingletonInitTest.cs b/tests/SkiaSharp.Tests.SingletonInit.Console/SKSingletonInitTest.cs new file mode 100644 index 00000000000..1d1ee680ec4 --- /dev/null +++ b/tests/SkiaSharp.Tests.SingletonInit.Console/SKSingletonInitTest.cs @@ -0,0 +1,33 @@ +using System; +using SkiaSharp; +using Xunit; + +namespace SkiaSharp.Tests.SingletonInit +{ + // Regression guard for https://github.com/mono/SkiaSharp/issues/3817. + // + // Touching SKFontManager.Default as the very first SkiaSharp type in a fresh + // process used to throw a TypeInitializationException: the SKObject static + // constructor eagerly initializes a cascade of singletons, and the SKTypeface + // initializer in that cascade read the managed SKFontManager.Default and + // SKFontStyle.Normal singletons. When SKFontManager was the first type touched, + // that re-entered the still-running SKFontManager/SKFontStyle initializer on the + // same thread and observed a not-yet-assigned (null) singleton, producing a + // NullReferenceException surfaced as a TypeInitializationException. + // + // This assembly intentionally contains a single test so that this is genuinely + // the first SkiaSharp access in the process. + public class SKSingletonInitTest + { + [Fact] + public void TouchingFontManagerDefaultFirstDoesNotThrow () + { + var fontManager = SKFontManager.Default; + + Assert.NotNull (fontManager); + Assert.NotEqual (IntPtr.Zero, fontManager.Handle); + Assert.NotEqual (IntPtr.Zero, SKFontStyle.Normal.Handle); + Assert.NotEqual (IntPtr.Zero, SKTypeface.Default.Handle); + } + } +} diff --git a/tests/SkiaSharp.Tests.SingletonInit.Console/SkiaSharp.Tests.SingletonInit.Console.csproj b/tests/SkiaSharp.Tests.SingletonInit.Console/SkiaSharp.Tests.SingletonInit.Console.csproj new file mode 100644 index 00000000000..ad157243aaa --- /dev/null +++ b/tests/SkiaSharp.Tests.SingletonInit.Console/SkiaSharp.Tests.SingletonInit.Console.csproj @@ -0,0 +1,38 @@ + + + + + + $(TFMCurrent) + $(TargetFrameworks);net48 + SkiaSharp.Tests.SingletonInit + SkiaSharp.Tests.SingletonInit + false + true + true + AnyCPU;x64;x86 + + + + + + + + + + + + + + + + + From 18fba7307035e92621b8727c99fcee2d8fea3b84 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Wed, 3 Jun 2026 00:23:31 +0200 Subject: [PATCH 16/29] Add concurrency/lifecycle corner-case tests for the singleton rework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardens test coverage around the reworked HandleDictionary/SKObject singleton lifecycle. These cases were discovered while exploring the lock-striping experiment (#4101/#4102); the experiment itself adds no measurable value, but the corner cases it surfaced are worth locking in against the single-global-lock design in this PR. Adds (tests only — no production changes): - SKHandleDictionaryTestHelpers.cs: shared internal fakes (FakeNativeObject, GatedCleanupObject) and deterministic concurrency helpers. RunConcurrent uses one dedicated Thread per participant with a Barrier (the thread pool can't guarantee N simultaneous Parallel.For bodies, which deadlocks against Barrier(N)); RunWithTimeout bounds dispose-storms so a production deadlock fails cleanly instead of hanging. AssertDeregistered asserts a handle no longer resolves to a specific wrapper by reference identity, which is safe against native address reuse under xUnit's parallel collections (asserting the address is simply absent is racy — a parallel test can reallocate a fresh wrapper at the same freed pointer). - SKHandleDictionaryReservationTest.cs: concurrent same-handle construction resolves to exactly one wrapper; waiters only observe a fully-constructed wrapper; factory failure clears the reservation and lets a waiter/retry reconstruct; cross-handle construction in opposite order does not deadlock under the single lock. - SKHandleDictionaryDisposeProtected.cs: dispose-protected construction sets the flag; a racing public Dispose() never tears down a dispose-protected wrapper. - SKHandleDictionaryStaleWrapperTest.cs: a stale wrapper deregistering after address reuse never evicts the live replacement (GetOrAdd and direct-ctor paths, including two stale wrappers vs a third replacement). - SKManagedStreamTest.cs / SKCodecTest.cs: exercise the managed-Stream callback path that the existing SKMemoryStream tests don't reach — public Dispose() ignored while a codec read is in flight; lazy decode after ownership transfer; non-seekable/front-buffered reparenting tears down the nested stream exactly once; failed Create closes the stream exactly once (seekable and non-seekable); PDF/SVG writer streams stay open until the owner is disposed; concurrent dispose of many/same managed streams is safe. Also corrects the InvalidStreamIsDisposedImmediately expectation in three files: the failed-create path runs RevokeOwnership(null) -> DisposeInternal(), which genuinely disposes (and deregisters) the wrapper rather than marking it IgnorePublicDispose. ff18b0f's blanket IsDisposed -> IgnorePublicDispose rename was right for the success/reparent path but wrong here, where there is no new owner to protect the wrapper for. Finally, fixes an intermittent flake in the existing ManagedStreamIsCollectedWhenTypefaceIsDisposed: it used the same racy absence assertion (Assert.False(GetInstance(handle, out _))) that fails when a parallel test reuses the freed native pointer. Switched to the reference-identity AssertDeregistered helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/Tests/SkiaSharp/SKCodecTest.cs | 36 +- .../SKHandleDictionaryDisposeProtectedTest.cs | 99 +++ .../SKHandleDictionaryReservationTest.cs | 172 ++++ .../SKHandleDictionaryStaleWrapperTest.cs | 252 ++++++ .../SKHandleDictionaryTestHelpers.cs | 180 ++++ tests/Tests/SkiaSharp/SKManagedStreamTest.cs | 800 +++++++++++++++++- tests/Tests/SkiaSharp/SKTypefaceTest.cs | 11 +- 7 files changed, 1546 insertions(+), 4 deletions(-) create mode 100644 tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs create mode 100644 tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs create mode 100644 tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs create mode 100644 tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs diff --git a/tests/Tests/SkiaSharp/SKCodecTest.cs b/tests/Tests/SkiaSharp/SKCodecTest.cs index 4e5d6035479..3c4a95ac7f6 100644 --- a/tests/Tests/SkiaSharp/SKCodecTest.cs +++ b/tests/Tests/SkiaSharp/SKCodecTest.cs @@ -116,8 +116,11 @@ public unsafe void InvalidStreamIsDisposedImmediately() Assert.Null(SKCodec.Create(stream)); + // Failed Create has no new native owner, so RevokeOwnership(null) runs + // DisposeInternal(): the wrapper is genuinely disposed and deregistered, + // not merely public-dispose-guarded. Hence IsDisposed (not IgnorePublicDispose). Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); + Assert.True(stream.IsDisposed); Assert.False(SKObject.GetInstance(handle, out _)); } @@ -492,5 +495,36 @@ public void CanDecodeData(string image) Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels)); Assert.NotEmpty(pixels); } + + [SkippableFact] + public void CanDecodeManagedStreamAfterCreate() + { + // Regression: SKCodec.Create(Stream) wraps the managed .NET stream and the + // codec decodes lazily. The underlying managed stream MUST stay readable + // until the codec is disposed — the singleton/lifecycle rework must not let + // the reparented wrapper's ownership transfer eagerly close the .NET stream. + using var stream = new MemoryStream(File.ReadAllBytes(Path.Combine(PathToImages, "baboon.png"))); + using var codec = SKCodec.Create(stream); + Assert.NotNull(codec); + + Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels)); + Assert.NotEmpty(pixels); + } + + [SkippableFact] + public void CanDecodeNonSeekableManagedStreamAfterCreate() + { + // Non-seekable managed streams route through SKFrontBufferedManagedStream + // (a nested SKManagedStream). The lazy decode must still reach the underlying + // .NET stream through the front buffer + nested wrapper without it being + // closed early by the ownership transfer. + using var backing = new MemoryStream(File.ReadAllBytes(Path.Combine(PathToImages, "baboon.png"))); + using var nonSeekable = new NonSeekableReadOnlyStream(backing); + using var codec = SKCodec.Create(nonSeekable); + Assert.NotNull(codec); + + Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels)); + Assert.NotEmpty(pixels); + } } } diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs new file mode 100644 index 00000000000..80846d559e0 --- /dev/null +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs @@ -0,0 +1,99 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using static SkiaSharp.Tests.SKHandleDictionaryTestHelpers; + +namespace SkiaSharp.Tests +{ + // Coverage for HandleDictionary's dispose-protected promotion (PR #4080, fixes #3817). + // + // GetOrAddObject(..., disposeProtected: true) calls SKObject.PreventPublicDisposal() on the + // returned wrapper while holding the instancesLock. The one-way IgnorePublicDispose latch is set + // under that lock; public Dispose() reads it (and pairs it with the isDisposed CAS) under the + // mutually-exclusive write lock. These white-box tests pin that a dispose-protected caller always + // gets back a live, protected wrapper, even when racing a concurrent public Dispose() for the same + // handle. They use a fake, non-owning SKObject (see SKHandleDictionaryTestHelpers) so no native + // memory is touched. + public class SKHandleDictionaryDisposeProtectedTest : SKTest + { + // --- dispose-protected fresh construction sets the IgnorePublicDispose latch --- + + [SkippableFact] + public void DisposeProtectedFreshConstructionSetsFlag () + { + var handle = NextHandle (); + + var obj = HandleDictionary.GetOrAddObject ( + handle, owns: false, unrefExisting: false, disposeProtected: true, + (h, o) => new FakeNativeObject (h)); + + try { + Assert.NotNull (obj); + Assert.True (obj.IgnorePublicDispose); + + // Public Dispose() is a no-op while protected. + obj.Dispose (); + Assert.False (obj.IsDisposed); + } finally { + // Tear down via the internal path that ignores the protection flag. + obj.DisposeInternal (); + } + } + + // --- Promote (disposeProtected) racing a concurrent public Dispose() on the SAME handle. + // PreventPublicDisposal (under the instancesLock) and Dispose()'s IgnorePublicDispose + // check + isDisposed CAS (under the write lock) are mutually exclusive, so the outcome is + // always well-defined and never torn: the disposeProtected caller ALWAYS gets back a live, + // protected wrapper (the original if it promoted in time, or a freshly reconstructed one if + // Dispose won the race). --- + + [SkippableFact] + public void DisposeProtectedRacingPublicDisposeNeverTears () + { + const int iterations = 300; + var created = new System.Collections.Concurrent.ConcurrentBag (); + + for (var i = 0; i < iterations; i++) { + var handle = NextHandle (); + + var original = HandleDictionary.GetOrAddObject ( + handle, owns: false, unrefExisting: false, + (h, o) => { + var obj = new FakeNativeObject (h); + created.Add (obj); + return obj; + }); + + FakeNativeObject promoted = null; + + // Two dedicated threads rendezvous, then race promote-vs-dispose on the SAME handle. + RunConcurrent (2, idx => { + if (idx == 0) { + // Promote: returns the live instance protected, or reconstructs if Dispose won. + promoted = HandleDictionary.GetOrAddObject ( + handle, owns: false, unrefExisting: false, disposeProtected: true, + (h, o) => { + var obj = new FakeNativeObject (h); + created.Add (obj); + return obj; + }); + } else { + original.Dispose (); + } + }, deadlockMessage: "Promote-vs-dispose race deadlocked."); + + // The disposeProtected contract: always a live, protected wrapper for this handle. + Assert.NotNull (promoted); + Assert.False (promoted.IsDisposed); + Assert.True (promoted.IgnorePublicDispose); + Assert.True (HandleDictionary.GetInstance (handle, out var current)); + Assert.Same (promoted, current); + } + + // Tear everything down via the internal path (ignores IgnorePublicDispose). + foreach (var obj in created) + obj.DisposeInternal (); + } + } +} diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs new file mode 100644 index 00000000000..015171592c8 --- /dev/null +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using static SkiaSharp.Tests.SKHandleDictionaryTestHelpers; + +namespace SkiaSharp.Tests +{ + // Corner-case coverage for concurrent construction in HandleDictionary (PR #4080, fixes #3817). + // + // #4080 serializes construction with a SINGLE global lock: GetOrAddObject holds the + // instancesLock (upgradeable-read) for its whole duration, INCLUDING the factory invocation. + // There is no per-handle reservation gate. These white-box tests drive HandleDictionary directly + // with a fake, non-owning SKObject (see SKHandleDictionaryTestHelpers) so the registry machinery + // can be exercised without any native allocations, pinning the observable invariants that this + // lock-the-whole-thing model guarantees: exactly-once construction, publication safety, and + // recovery after a throwing factory. + // + // NOTE: nested/re-entrant GetOrAddObject from inside a factory is intentionally NOT tested here. + // Because the factory runs under the non-recursive lock (ReaderWriterLockSlim with + // LockRecursionPolicy.NoRecursion on non-Windows), a nested GetOrAddObject would throw + // LockRecursionException rather than recurse — and on Windows (recursive CRITICAL_SECTION) it + // would not — so any such test is platform-dependent and is omitted by design. + public class SKHandleDictionaryReservationTest : SKTest + { + // --- Exactly-once construction under concurrency --- + + [SkippableFact] + public void ConcurrentSameHandleConstructsExactlyOnce () + { + var handle = NextHandle (); + var factoryCalls = 0; + + const int threadCount = 32; + var results = new FakeNativeObject[threadCount]; + + RunConcurrent (threadCount, i => { + results[i] = HandleDictionary.GetOrAddObject ( + handle, owns: false, unrefExisting: false, + (h, o) => { + Interlocked.Increment (ref factoryCalls); + return new FakeNativeObject (h); + }); + }, deadlockMessage: "Concurrent same-handle construction deadlocked."); + + try { + Assert.Equal (1, factoryCalls); + for (var i = 0; i < threadCount; i++) { + Assert.NotNull (results[i]); + Assert.Same (results[0], results[i]); + } + } finally { + results[0].Dispose (); + } + } + + // --- Publication safety: waiters never observe a half-built wrapper --- + + [SkippableFact] + public void WaitersOnlyObserveFullyConstructedWrapper () + { + var handle = NextHandle (); + + const int threadCount = 16; + using var startGate = new ManualResetEventSlim (false); + var results = new FakeNativeObject[threadCount]; + + var threads = new List (); + for (var i = 0; i < threadCount; i++) { + var index = i; + var t = new Thread (() => { + startGate.Wait (); + results[index] = HandleDictionary.GetOrAddObject ( + handle, owns: false, unrefExisting: false, + (h, o) => new FakeNativeObject (h, duringConstruction: () => Thread.Sleep (40))); + }); + t.IsBackground = true; + t.Start (); + threads.Add (t); + } + + startGate.Set (); + foreach (var t in threads) + Assert.True (t.Join (10_000), "Possible deadlock waiting on the construction lock."); + + try { + for (var i = 0; i < threadCount; i++) { + Assert.NotNull (results[i]); + Assert.Same (results[0], results[i]); + // If a waiter ever saw the object before its ctor finished, this would be 0. + Assert.Equal (1, results[i].FullyConstructed); + } + } finally { + results[0].Dispose (); + } + } + + // --- A throwing factory leaves nothing registered and lets a later call reconstruct --- + + [SkippableFact] + public void FactoryFailureLeavesNothingRegisteredAndAllowsReconstruction () + { + var handle = NextHandle (); + + Assert.Throws (() => + HandleDictionary.GetOrAddObject ( + handle, owns: false, unrefExisting: false, + (h, o) => throw new InvalidOperationException ("boom"))); + + // The factory threw under the lock; the lock is released in the finally and nothing was + // ever registered, so no later caller is stranded. + Assert.False (HandleDictionary.GetInstance (handle, out _)); + + var rebuilt = HandleDictionary.GetOrAddObject ( + handle, owns: false, unrefExisting: false, + (h, o) => new FakeNativeObject (h)); + + try { + Assert.NotNull (rebuilt); + Assert.True (HandleDictionary.GetInstance (handle, out var fetched)); + Assert.Same (rebuilt, fetched); + } finally { + rebuilt.Dispose (); + } + } + + // --- Concurrent storm where the FIRST owner's factory fails: a waiter recovers --- + + [SkippableFact] + public void ConcurrentFactoryFailureRecoveredByWaiter () + { + var handle = NextHandle (); + var failFirst = 1; + var successfulFactoryCalls = 0; + + const int threadCount = 16; + var results = new FakeNativeObject[threadCount]; + + RunConcurrent (threadCount, i => { + try { + results[i] = HandleDictionary.GetOrAddObject ( + handle, owns: false, unrefExisting: false, + (h, o) => { + // Exactly one factory invocation throws; the registry must recover. + if (Interlocked.CompareExchange (ref failFirst, 0, 1) == 1) + throw new InvalidOperationException ("first owner fails"); + Interlocked.Increment (ref successfulFactoryCalls); + return new FakeNativeObject (h); + }); + } catch (InvalidOperationException) { + results[i] = null; + } + }, deadlockMessage: "Concurrent factory-failure recovery deadlocked."); + + FakeNativeObject survivor = null; + try { + // At least one thread saw the failure; everyone who got an object got the SAME one. + for (var i = 0; i < threadCount; i++) { + if (results[i] == null) + continue; + survivor ??= results[i]; + Assert.Same (survivor, results[i]); + } + Assert.NotNull (survivor); + Assert.Equal (1, successfulFactoryCalls); + } finally { + survivor?.Dispose (); + } + } + } +} diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs new file mode 100644 index 00000000000..a756a414a0f --- /dev/null +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs @@ -0,0 +1,252 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using static SkiaSharp.Tests.SKHandleDictionaryTestHelpers; + +namespace SkiaSharp.Tests +{ + // Coverage for HandleDictionary's stale-wrapper deregistration window (PR #4080, fixes #3817). + // + // SKObject.Dispose() claims the isDisposed flag under the write lock, RELEASES the lock, runs + // DisposeManaged()/DisposeNative() OUTSIDE the lock, and only then sets Handle = 0 -> + // DeregisterHandle(). A handle can therefore be reused (ABA) by a brand-new wrapper while the old, + // already-disposed wrapper is still parked mid-cleanup. These white-box tests park a wrapper in that + // exact window (via GatedCleanupObject) and prove the late DeregisterHandle of the stale wrapper + // never evicts the live replacement and never raises a THROW_OBJECT_EXCEPTIONS diagnostic. They use + // a fake, non-owning SKObject (see SKHandleDictionaryTestHelpers) so no native memory is touched. + public class SKHandleDictionaryStaleWrapperTest : SKTest + { + // --- Address reuse (ABA): a handle freed then re-used for a new native object. The registry + // must hand out a fresh wrapper, not the disposed one. --- + + [SkippableFact] + public void AddressReuseAfterDisposeConstructsFreshWrapper () + { + var handle = NextHandle (); + + var first = HandleDictionary.GetOrAddObject ( + handle, owns: false, unrefExisting: false, + (h, o) => new FakeNativeObject (h)); + Assert.NotNull (first); + Assert.True (HandleDictionary.GetInstance (handle, out var fetchedFirst)); + Assert.Same (first, fetchedFirst); + + // Free the handle: public Dispose deregisters it from the registry. + first.Dispose (); + Assert.True (first.IsDisposed); + Assert.False (HandleDictionary.GetInstance (handle, out _)); + + // A new native object reuses the same pointer value: must build a brand-new wrapper. + var second = HandleDictionary.GetOrAddObject ( + handle, owns: false, unrefExisting: false, + (h, o) => new FakeNativeObject (h)); + + try { + Assert.NotNull (second); + Assert.NotSame (first, second); + Assert.False (second.IsDisposed); + Assert.True (HandleDictionary.GetInstance (handle, out var fetchedSecond)); + Assert.Same (second, fetchedSecond); + } finally { + second.Dispose (); + } + } + + // --- Stale wrapper finishing cleanup OUTSIDE the lock must not evict a replacement that took + // over the SAME (reused) handle while it was parked mid-cleanup. This pins all three safety + // claims in SKObject.Dispose()'s comment for the "cleanup runs outside the lock" window: + // #1 a concurrent GetOrAddObject after the isDisposed CAS sees the disposed wrapper and is + // filtered out (GetInstanceNoLocks), so it builds a fresh wrapper; + // #3 the stale wrapper's own Handle=0 -> DeregisterHandle is a no-op when the entry now + // points to someone else (release), and raises no THROW_OBJECT_EXCEPTIONS diagnostic + // (the deregistering instance is itself already disposed). + // Variant below covers claim #2 (RegisterHandle's replacement branch). --- + + [SkippableFact] + public void StaleWrapperCleanupDoesNotEvictGetOrAddReplacementForReusedHandle () + { + var handle = NextHandle (); + + using var enteredCleanup = new ManualResetEventSlim (false); + using var releaseCleanup = new ManualResetEventSlim (false); + + var first = new GatedCleanupObject (handle, enteredCleanup, releaseCleanup); + Assert.True (HandleDictionary.GetInstance (handle, out var fetchedFirst)); + Assert.Same (first, fetchedFirst); + + GatedCleanupObject second = null; + try { + // Public-dispose W1 on another thread: it claims isDisposed under the write lock, + // releases the lock, then parks in DisposeManaged BEFORE Handle=0 -> DeregisterHandle runs. + var disposer = Task.Run (() => first.Dispose ()); + try { + Assert.True (enteredCleanup.Wait (10_000), "W1 never entered the managed-cleanup window."); + Assert.True (first.IsDisposed); + + // While W1 is parked mid-cleanup, a new native object reuses the handle. Run the + // construction under a timeout: if cleanup ever regressed to holding the lock, + // this would block on the lock and we want a deterministic failure, not a suite hang. + var creator = Task.Run (() => HandleDictionary.GetOrAddObject ( + handle, owns: false, unrefExisting: false, (h, o) => new GatedCleanupObject (h))); + Assert.True (creator.Wait (10_000), "Replacement construction blocked — cleanup may hold the lock."); + second = creator.Result; + + // GetInstanceNoLocks filtered the disposed W1, so a brand-new wrapper was published. + Assert.NotNull (second); + Assert.NotSame (first, second); + Assert.False (second.IsDisposed); + Assert.True (HandleDictionary.GetInstance (handle, out var afterReplace)); + Assert.Same (second, afterReplace); + } finally { + // Let W1 finish: Handle=0 -> DeregisterHandle(W1) must NOT evict W2 and must NOT throw. + releaseCleanup.Set (); + Assert.True (disposer.Wait (10_000), "W1 dispose did not complete."); + } + + // W2 survived the stale wrapper's deregistration and is still the registered instance. + Assert.True (HandleDictionary.GetInstance (handle, out var survivor)); + Assert.Same (second, survivor); + Assert.False (second.IsDisposed); + } finally { + // Dispose the replacement on every path so an assertion failure mid-window cannot leak + // a live wrapper and have GarbageCleanupFixture mask the real failure. + second?.Dispose (); + } + + Assert.False (HandleDictionary.GetInstance (handle, out _)); + } + + // --- Same window as above, but the replacement is constructed via a DIRECT constructor (no + // GetOrAddObject), so it travels through RegisterHandle. This pins claim #2: RegisterHandle's + // replacement branch only disposes an existing entry when it is NOT disposed (line guarded by + // !obj.IsDisposed) — a stale disposed W1 is simply overwritten, never recursively disposed, + // and W1's later DeregisterHandle still no-ops without evicting W2 or throwing. --- + + [SkippableFact] + public void StaleWrapperCleanupDoesNotEvictDirectCtorReplacementForReusedHandle () + { + var handle = NextHandle (); + + using var enteredCleanup = new ManualResetEventSlim (false); + using var releaseCleanup = new ManualResetEventSlim (false); + + var first = new GatedCleanupObject (handle, enteredCleanup, releaseCleanup); + Assert.True (HandleDictionary.GetInstance (handle, out var fetchedFirst)); + Assert.Same (first, fetchedFirst); + + GatedCleanupObject second = null; + try { + var disposer = Task.Run (() => first.Dispose ()); + try { + Assert.True (enteredCleanup.Wait (10_000), "W1 never entered the managed-cleanup window."); + Assert.True (first.IsDisposed); + + // Direct-construct the replacement: its base ctor RegisterHandle sees the disposed W1 + // entry, skips the dispose-the-old branch (!obj.IsDisposed is false), and overwrites it. + // Timeout-guarded so a cleanup-holds-the-lock regression fails deterministically. + var creator = Task.Run (() => new GatedCleanupObject (handle)); + Assert.True (creator.Wait (10_000), "Replacement construction blocked — cleanup may hold the lock."); + second = creator.Result; + + Assert.NotNull (second); + Assert.NotSame (first, second); + Assert.False (second.IsDisposed); + Assert.True (HandleDictionary.GetInstance (handle, out var afterReplace)); + Assert.Same (second, afterReplace); + } finally { + releaseCleanup.Set (); + Assert.True (disposer.Wait (10_000), "W1 dispose did not complete."); + } + + Assert.True (HandleDictionary.GetInstance (handle, out var survivor)); + Assert.Same (second, survivor); + Assert.False (second.IsDisposed); + } finally { + second?.Dispose (); + } + + Assert.False (HandleDictionary.GetInstance (handle, out _)); + } + + // --- Two stale wrappers in flight at once: W1 and W2 are BOTH parked in DisposeManaged (disposed, + // lock released, Handle not yet zeroed) for the same reused handle, while a live W3 owns the + // registry slot. When the two stale wrappers later run DeregisterHandle — in an order DIFFERENT + // from how they were published — neither may evict W3 nor throw. This stresses the "only remove + // if the entry still points to THIS instance" guard across overlapping stale deregistrations, + // which the single-stale-wrapper tests above do not exercise. --- + + [SkippableFact] + public void TwoStaleWrappersDeregisteringDoNotEvictThirdReplacementForReusedHandle () + { + var handle = NextHandle (); + + using var entered1 = new ManualResetEventSlim (false); + using var release1 = new ManualResetEventSlim (false); + using var entered2 = new ManualResetEventSlim (false); + using var release2 = new ManualResetEventSlim (false); + + var w1 = new GatedCleanupObject (handle, entered1, release1); + GatedCleanupObject w2 = null, w3 = null; + Task disposer1 = null, disposer2 = null; + + try { + // Park W1 (disposed) in its cleanup window. + disposer1 = Task.Run (() => w1.Dispose ()); + Assert.True (entered1.Wait (10_000), "W1 never entered the managed-cleanup window."); + Assert.True (w1.IsDisposed); + + // Overwrite the disposed W1 with W2 (direct ctor -> RegisterHandle overwrite-stale branch), + // then park W2 (disposed) in ITS cleanup window too. Now two stale wrappers are pending. + var make2 = Task.Run (() => new GatedCleanupObject (handle, entered2, release2)); + Assert.True (make2.Wait (10_000), "W2 construction blocked — cleanup may hold the lock."); + w2 = make2.Result; + Assert.NotSame (w1, w2); + Assert.True (HandleDictionary.GetInstance (handle, out var afterW2)); + Assert.Same (w2, afterW2); + + disposer2 = Task.Run (() => w2.Dispose ()); + Assert.True (entered2.Wait (10_000), "W2 never entered the managed-cleanup window."); + Assert.True (w2.IsDisposed); + + // Overwrite the disposed W2 with the live W3. + var make3 = Task.Run (() => new GatedCleanupObject (handle)); + Assert.True (make3.Wait (10_000), "W3 construction blocked — cleanup may hold the lock."); + w3 = make3.Result; + Assert.NotSame (w1, w3); + Assert.NotSame (w2, w3); + Assert.False (w3.IsDisposed); + Assert.True (HandleDictionary.GetInstance (handle, out var afterW3)); + Assert.Same (w3, afterW3); + + try { + // Drain the two stale wrappers in an order DIFFERENT from publication (W2 before W1). + // Each runs Handle=0 -> DeregisterHandle; the entry is the live W3, so both must no-op. + release2.Set (); + Assert.True (disposer2.Wait (10_000), "W2 dispose did not complete."); + release1.Set (); + Assert.True (disposer1.Wait (10_000), "W1 dispose did not complete."); + } finally { + // Belt-and-braces in case an assertion above threw before the ordered drain. + release1.Set (); + release2.Set (); + } + + // Both stale deregistrations ran; W3 must still be the registered instance. + Assert.True (HandleDictionary.GetInstance (handle, out var survivor)); + Assert.Same (w3, survivor); + Assert.False (w3.IsDisposed); + } finally { + // Unblock anything still parked and make sure all wrappers are drained/disposed so an early + // assertion failure cannot leak a wrapper and have GarbageCleanupFixture mask the real cause. + release1.Set (); + release2.Set (); + disposer1?.Wait (10_000); + disposer2?.Wait (10_000); + w3?.Dispose (); + } + + Assert.False (HandleDictionary.GetInstance (handle, out _)); + } + } +} diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs new file mode 100644 index 00000000000..48dd2b7e18f --- /dev/null +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs @@ -0,0 +1,180 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace SkiaSharp.Tests +{ + // Shared white-box fakes and helpers for the HandleDictionary corner-case tests + // (SKHandleDictionaryReservationTest / DisposeProtectedTest / StaleWrapperTest). + // + // These drive HandleDictionary directly with a fake, non-owning SKObject so the registry + // machinery (single-lock construction, dispose-protected promotion, stale-wrapper deregistration) + // can be exercised without any native allocations. A non-owning (owns: false) wrapper never + // calls a native free on Dispose, and the synthetic handle values are chosen far from any real + // native pointer range, so they cannot collide with live Skia objects. Every test disposes + // everything it registers so GarbageCleanupFixture stays clean. + // + // The fakes are internal top-level types (not private nested) so they can be reused across the + // three split test files. + + // A wrapper over a synthetic handle that touches no native memory. + internal sealed class FakeNativeObject : SKObject + { + // Set to 1 only AFTER the base ctor (and any "subclass init") has run. Waiters that dedup + // this instance must observe a fully-constructed object, i.e. FullyConstructed == 1. + public readonly int FullyConstructed; + + public FakeNativeObject (IntPtr handle, Action duringConstruction = null) + : base (handle, owns: false) + { + // Simulate subclass initialization happening after the base ctor's RegisterHandle. + duringConstruction?.Invoke (); + FullyConstructed = 1; + } + + protected override void DisposeNative () + { + // no native object backs this fake handle + } + } + + // A non-owning wrapper whose managed-cleanup phase can be parked on a gate. DisposeManaged runs + // AFTER SKObject.Dispose() has claimed the disposal (isDisposed CAS) and RELEASED the lock, but + // BEFORE Handle is zeroed (which triggers DeregisterHandle(this)). Parking here lets a test + // interleave a replacement registration for the SAME handle into the exact "cleanup runs outside + // the lock" window that SKObject.Dispose()'s comment claims is safe. + internal sealed class GatedCleanupObject : SKObject + { + private readonly ManualResetEventSlim enteredCleanup; + private readonly ManualResetEventSlim releaseCleanup; + + public GatedCleanupObject ( + IntPtr handle, + ManualResetEventSlim enteredCleanup = null, + ManualResetEventSlim releaseCleanup = null) + : base (handle, owns: false) + { + this.enteredCleanup = enteredCleanup; + this.releaseCleanup = releaseCleanup; + } + + protected override void DisposeNative () + { + // no native object backs this fake handle + } + + protected override void DisposeManaged () + { + if (enteredCleanup == null) + return; + + // Signal that we are parked in the post-claim / pre-deregister window, then wait for the + // test to release us. A timeout guards against a test bug leaving this thread parked forever. + enteredCleanup.Set (); + releaseCleanup?.Wait (30_000); + } + } + + internal static class SKHandleDictionaryTestHelpers + { + private static long handleSeed = 0x6000_0000_0000_0000L; + + // Hands out unique synthetic handles well away from any real native pointer range. + public static IntPtr NextHandle () => + new IntPtr (Interlocked.Add (ref handleSeed, 0x1000)); + + // Runs body(0..threadCount-1) on that many DEDICATED threads that all rendezvous at an internal + // Barrier before invoking the body, guaranteeing genuine simultaneity. This is deliberately NOT + // Parallel.For / Parallel.Invoke / Task.Run: those draw workers from the thread pool, whose degree + // of parallelism depends on pool availability. A Barrier(N) needs N participants present AT ONCE, + // so under a loaded full-suite run the pool may fail to supply N workers in time and the Barrier + // hangs (a non-deterministic, load-dependent deadlock). Dedicated threads always reach the barrier. + // Every thread is joined against a shared deadline; a real (production) deadlock surfaces as a + // failed Join rather than a hung suite. The first body exception (if any) is rethrown after join. + public static void RunConcurrent ( + int threadCount, + Action body, + int timeoutMs = 30_000, + string deadlockMessage = "Concurrent operation deadlocked.") + { + using var barrier = new Barrier (threadCount); + var errors = new System.Collections.Concurrent.ConcurrentQueue (); + var threads = new Thread[threadCount]; + + for (var i = 0; i < threadCount; i++) { + var index = i; + threads[i] = new Thread (() => { + try { + barrier.SignalAndWait (); + body (index); + } catch (Exception ex) { + errors.Enqueue (ex); + } + }) { IsBackground = true }; + } + + foreach (var t in threads) + t.Start (); + + // Attempt to join every worker before asserting so that a single timed-out + // thread cannot leave the others unjoined (a worker still holding + // instancesLock would otherwise cascade into later-test hangs). + var deadline = Environment.TickCount64 + timeoutMs; + var allJoined = true; + foreach (var t in threads) { + var remaining = (int) Math.Max (0, deadline - Environment.TickCount64); + allJoined &= t.Join (remaining); + } + + Assert.True (allJoined, deadlockMessage); + + if (errors.TryDequeue (out var first)) + throw first; + } + + // Runs a body that internally spawns its own parallelism (e.g. Parallel.For) on a dedicated + // thread and joins with a deadline, so a production-side dispose/lock deadlock surfaces as a + // deterministic test FAILURE instead of hanging the whole suite. Exceptions thrown by the body + // are captured and rethrown on the calling thread. + public static void RunWithTimeout ( + Action body, + int timeoutMs = 30_000, + string deadlockMessage = "Operation deadlocked.") + { + Exception captured = null; + var runner = new Thread (() => { + try { + body (); + } catch (Exception ex) { + captured = ex; + } + }) { IsBackground = true }; + + runner.Start (); + Assert.True (runner.Join (timeoutMs), deadlockMessage); + + if (captured != null) + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (captured); + } + + // Asserts that a disposed wrapper has been deregistered from the HandleDictionary. + // + // The handle is a raw native pointer. Once the wrapper is disposed and its native object is + // freed, the allocator is free to immediately hand that exact address back out for an unrelated + // object created by a DIFFERENT test running concurrently (xUnit parallelizes test collections). + // Asserting the address is entirely absent from the registry is therefore racy and can spuriously + // fail when a parallel test re-registers a brand-new wrapper at the reused address. The real + // lifecycle invariant we care about is narrower and address-reuse-safe: OUR specific disposed + // wrapper must no longer be reachable through the registry under its handle. A different live + // wrapper that legitimately reused the freed address is fine and must not fail the test. + public static void AssertDeregistered (IntPtr handle, TSkiaObject instance) + where TSkiaObject : SKObject + { + if (HandleDictionary.GetInstance (handle, out var found)) + Assert.False ( + ReferenceEquals (found, instance), + $"Disposed {typeof (TSkiaObject).Name} is still registered under handle 0x{handle.ToString ("x")}."); + } + } +} diff --git a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs index 94071469874..b96ab14fce2 100644 --- a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs +++ b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs @@ -3,6 +3,8 @@ using System.Linq; using Xunit; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace SkiaSharp.Tests { @@ -360,8 +362,11 @@ public unsafe void InvalidStreamIsDisposedImmediately() Assert.Null(SKCodec.Create(stream)); + // Failed Create has no new native owner, so RevokeOwnership(null) runs + // DisposeInternal(): the wrapper is genuinely disposed and deregistered, + // not merely public-dispose-guarded. Hence IsDisposed (not IgnorePublicDispose). Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); + Assert.True(stream.IsDisposed); Assert.False(SKObject.GetInstance(handle, out _)); } @@ -653,5 +658,798 @@ public void DotNetStreamIsClosedWhenSKManagedStreamIsDisposed() dupe.Dispose(); } + + [SkippableFact] + public unsafe void StreamLosesOwnershipButManagedStreamStaysOpenUntilOwnerDisposed() + { + var path = Path.Combine(PathToImages, "color-wheel.png"); + var bytes = File.ReadAllBytes(path); + var dotnetStream = new MemoryStream(bytes); + var stream = new SKManagedStream(dotnetStream, true); + var handle = stream.Handle; + + Assert.True(stream.OwnsHandle); + Assert.False(stream.IsDisposed); + Assert.True(SKObject.GetInstance(handle, out _)); + + var codec = SKCodec.Create(stream); + + // The codec reads the managed stream LAZILY (on GetPixels), so the + // wrapper must NOT be disposed at ownership transfer: doing so would + // close the underlying .NET stream and crash the later managed read. + Assert.False(stream.OwnsHandle); + Assert.False(stream.IsDisposed); + Assert.True(stream.IgnorePublicDispose); + Assert.True(SKObject.GetInstance(handle, out _)); + Assert.True(dotnetStream.CanRead); + + // A public Dispose() is ignored while the codec owns the native stream. + stream.Dispose(); + Assert.False(stream.IsDisposed); + Assert.True(dotnetStream.CanRead); + + // The lazy managed read must succeed — this is the regression guard. + Assert.Equal(SKCodecResult.Success, codec.GetPixels(out var pixels)); + Assert.NotEmpty(pixels); + + // Disposing the owner tears down the wrapper and closes the .NET stream + // (disposeManagedStream: true), now that nothing reads it any more. + codec.Dispose(); + Assert.True(stream.IsDisposed); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + Assert.False(dotnetStream.CanRead); + } + + // The SKObject.Owned write-stream path (used by SKSvgCanvas.Create(Stream) and + // SKDocument.Create*(Stream)) is the mirror image of the codec/typeface read path. + // There the internally-created SKManagedWStream is registered as an OwnsHandle child, + // so it is torn down in DisposeManaged() — AFTER DisposeNative(). That ordering is + // load-bearing for write streams: the native object (e.g. the SVG canvas footer + // writer, or the PDF document serializer) flushes its final bytes into the managed + // stream as it is destroyed, so the .NET stream MUST still be open at DisposeNative + // time. These tests lock in that guarantee and prove writes triggered AFTER Create + // (lazy writes) reach the underlying .NET stream without crossing a closed boundary. + + [SkippableFact] + public void SvgCanvasWritesLazilyAndKeepsManagedStreamOpenUntilOwnerDisposed() + { + var tracking = new LifecycleTrackingStream(); + + var svg = SKSvgCanvas.Create(SKRect.Create(100, 100), tracking); + + // Create must NOT dispose or close the caller's stream. + Assert.False(tracking.IsDisposed); + Assert.True(tracking.CanWrite); + + // Drawing happens AFTER Create returns: this is a lazy write through the + // still-alive owned wstream. It must not crash or hit a closed stream. + using (var paint = new SKPaint { Color = SKColors.Red, Style = SKPaintStyle.Fill }) + { + svg.DrawRect(SKRect.Create(10, 10, 80, 80), paint); + } + Assert.False(tracking.IsDisposed); + Assert.True(tracking.CanWrite); + + // Disposing the owner runs DisposeNative() (native canvas flushes the SVG + // footer into the managed stream) BEFORE the owned wstream's DisposeManaged(). + // The final native flush must therefore land in a still-open .NET stream. + svg.Dispose(); + + // disposeManagedStream defaults to false for the Stream overload, so the + // caller's stream stays open after the canvas is gone. + Assert.False(tracking.IsDisposed); + Assert.True(tracking.BytesWritten > 0); + Assert.False(tracking.WroteAfterClose); + + tracking.Position = 0; + using var reader = new StreamReader(tracking); + var xml = reader.ReadToEnd(); + Assert.Contains(" 0); + + // Disposing the owner tears down the owned wstream in DisposeManaged(), after + // DisposeNative(). With disposeManagedStream: false the .NET stream survives. + document.Dispose(); + Assert.False(tracking.IsDisposed); + Assert.False(tracking.WroteAfterClose); + + var header = new byte[5]; + tracking.Position = 0; + Assert.Equal(5, tracking.Read(header, 0, 5)); + Assert.Equal("%PDF-", System.Text.Encoding.ASCII.GetString(header)); + } + + // ---- fromNative destroy-callback re-entrancy (mirrors the SKDrawable invariant) ---- + // SKManagedStream / SKManagedWStream own a native object whose destruction triggers a + // managed destroy proxy (DelegateProxies.*stream*) that flips `fromNative` to 1 and calls + // Dispose() re-entrantly. Under the lock-paired SKObject.Dispose, DisposeNative() runs + // OUTSIDE the lock, so the synchronous native destroy can re-enter Dispose() on the + // same thread; the re-entrant call re-acquires the lock fresh and no-ops on isDisposed==1. + // These tests pin the single-free + deregistration + fromNative-flip invariants. + + [SkippableFact] + public void DisposingManagedStreamFiresNativeDestroyCallback() + { + var dotnet = CreateTestStream(); + var stream = new SKManagedStream(dotnet, true); + var handle = stream.Handle; + + try + { + Assert.NotEqual(IntPtr.Zero, handle); + Assert.Equal(0, stream.fromNative); + Assert.True(HandleDictionary.GetInstance(handle, out var live)); + Assert.Same(stream, live); + } + finally + { + stream.Dispose(); + } + + Assert.Equal(1, stream.fromNative); + Assert.True(stream.IsDisposed); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + + [SkippableFact] + public void DisposingManagedWStreamFiresNativeDestroyCallback() + { + var dotnet = new MemoryStream(); + var stream = new SKManagedWStream(dotnet, true); + var handle = stream.Handle; + + try + { + Assert.NotEqual(IntPtr.Zero, handle); + Assert.Equal(0, stream.fromNative); + Assert.True(HandleDictionary.GetInstance(handle, out var live)); + Assert.Same(stream, live); + } + finally + { + stream.Dispose(); + } + + Assert.Equal(1, stream.fromNative); + Assert.True(stream.IsDisposed); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + + [SkippableFact] + public void DisposingManagedStreamTwiceIsNoOp() + { + var stream = new SKManagedStream(CreateTestStream(), true); + var handle = stream.Handle; + + stream.Dispose(); + Assert.Equal(1, stream.fromNative); + Assert.True(stream.IsDisposed); + + stream.Dispose(); + Assert.Equal(1, stream.fromNative); + Assert.True(stream.IsDisposed); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + + [SkippableFact] + public void ConcurrentDisposeOfSameManagedStreamIsIdempotent() + { + // Many threads racing Dispose() on the SAME wrapper must funnel through the single + // isDisposed CAS: one cleanup, native freed once, destroy callback flips fromNative once. + var stream = new SKManagedStream(CreateTestStream(), true); + var handle = stream.Handle; + + SKHandleDictionaryTestHelpers.RunWithTimeout( + () => Parallel.For(0, 64, _ => stream.Dispose()), + deadlockMessage: "Concurrent Dispose() of the same managed stream deadlocked."); + + Assert.Equal(1, stream.fromNative); + Assert.True(stream.IsDisposed); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + + [SkippableFact] + public void ConcurrentDisposeOfManyManagedStreamsIsSafe() + { + const int count = 128; + var streams = new List(count); + for (var i = 0; i < count; i++) + streams.Add(new SKManagedStream(CreateTestStream(), true)); + + SKHandleDictionaryTestHelpers.RunWithTimeout( + () => Parallel.For(0, count, i => streams[i].Dispose()), + deadlockMessage: "Concurrent Dispose() of many managed streams deadlocked."); + + foreach (var stream in streams) + { + Assert.Equal(1, stream.fromNative); + Assert.True(stream.IsDisposed); + } + } + + // After SKCodec.Create reparents the managed stream (OwnsHandle=false, IgnorePublicDispose + // latched under the lock), the user STILL holds the wrapper and may call Dispose() on it + // from another thread. The PR's lock-paired public Dispose must IGNORE that call so the codec + // keeps reading through a live stream and remains its sole disposer. Before the LAZY-reparent + // fix an eager public close here tore the native stream out from under an in-flight native read + // -> use-after-free / host crash. The existing concurrency tests only race Dispose() against + // itself on an OWNED stream; these two pin the reparented supported concurrency: (1) an ignored + // public Dispose racing a lazy native read, and (2) an ignored public Dispose racing the codec's + // owner-teardown (which must still be the single, exactly-once disposer of the wrapper). + + [SkippableFact] + public unsafe void PublicDisposeWhileCodecReadIsInFlightIsIgnored() + { + // DETERMINISTIC version of the race: park the native lazy read INSIDE the .NET + // Stream.Read() callback, fire the (reparented) wrapper's public Dispose() while the + // native read is provably in-flight, then release the read. The public Dispose must be + // ignored (IgnorePublicDispose latched), so the in-flight native read completes against a + // live stream and GetPixels succeeds. This is the exact use-after-free the LAZY-reparent + // fix prevents — an eager public close here would close the .NET stream mid-read. + var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); + + using var readEntered = new ManualResetEventSlim(false); + using var releaseRead = new ManualResetEventSlim(false); + + var dotnet = new GatedCountingStream(bytes); + var stream = new SKManagedStream(dotnet, true); + var handle = stream.Handle; + var codec = SKCodec.Create(stream); + Task reader = null; + try + { + Assert.False(stream.OwnsHandle); + Assert.True(stream.IgnorePublicDispose); + + // Arm only AFTER Create() so the gate trips on a GetPixels read, not header parsing. + dotnet.Arm(readEntered, releaseRead); + + var result = SKCodecResult.InternalError; + reader = Task.Run(() => result = codec.GetPixels(out _)); + + Assert.True(readEntered.Wait(TimeSpan.FromSeconds(30)), "Native lazy read never entered the managed stream."); + + // The native read is parked inside Read(); a public Dispose now must be a no-op. + stream.Dispose(); + Assert.False(stream.IsDisposed); + + releaseRead.Set(); + Assert.True(reader.Wait(TimeSpan.FromSeconds(30)), "GetPixels did not complete after the read was released."); + + // If the ignored Dispose had wrongly closed the .NET stream, the parked inner.Read would + // have thrown and GetPixels would not be Success; DisposeCount would also not be 0. + Assert.Equal(SKCodecResult.Success, result); + Assert.False(stream.IsDisposed); + Assert.Equal(0, dotnet.DisposeCount); + } + finally + { + // Drain the reader before tearing the codec down so no background decode runs against a + // disposed codec. Swallow here so a primary in-try assertion failure is not masked. + releaseRead.Set(); + try { reader?.Wait(TimeSpan.FromSeconds(30)); } catch { } + codec.Dispose(); + } + + // The codec is the sole disposer; the underlying .NET stream was closed exactly once. + Assert.True(stream.IsDisposed); + Assert.Equal(1, dotnet.DisposeCount); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + + [SkippableFact] + public unsafe void PublicDisposeAroundLazyCodecReadIsIgnoredStress() + { + // STRESS version: barrier-synchronised public Dispose vs a full GetPixels, repeated many + // times to shake out interleavings beyond the single overlap the deterministic gate pins. + // (The barrier only co-starts the two operations; it does NOT guarantee Dispose overlaps an + // in-flight Read — that exact overlap is proven deterministically by the gated test above.) + const int iterations = 100; + var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); + + for (var i = 0; i < iterations; i++) + { + var stream = new SKManagedStream(new MemoryStream(bytes), true); + var handle = stream.Handle; + var codec = SKCodec.Create(stream); + try + { + Assert.False(stream.OwnsHandle); + Assert.True(stream.IgnorePublicDispose); + + var result = SKCodecResult.InternalError; + using (var barrier = new Barrier(2)) + { + // Dedicated threads (not Task.Run) so the Barrier(2) always has both participants + // present even under full-suite load; see RunConcurrent rationale. + Exception readError = null, disposeError = null; + var read = new Thread (() => { try { barrier.SignalAndWait (); result = codec.GetPixels (out _); } catch (Exception ex) { readError = ex; } }) { IsBackground = true }; + var dispose = new Thread (() => { try { barrier.SignalAndWait (); stream.Dispose (); } catch (Exception ex) { disposeError = ex; } }) { IsBackground = true }; + read.Start (); + dispose.Start (); + var completed = read.Join (TimeSpan.FromSeconds (30)); + completed &= dispose.Join (TimeSpan.FromSeconds (30)); + Assert.True (completed, "Dispose/GetPixels race did not complete in time."); + if (readError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (readError); + if (disposeError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (disposeError); + } + + // The lazy native read saw a live stream; the racing public Dispose was a no-op. + Assert.Equal(SKCodecResult.Success, result); + Assert.False(stream.IsDisposed); + Assert.True(HandleDictionary.GetInstance(handle, out _)); + } + finally + { + codec.Dispose(); + } + + // The codec is the sole disposer; teardown ran after the race. + Assert.Equal(1, stream.fromNative); + Assert.True(stream.IsDisposed); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + } + + [SkippableFact] + public unsafe void PublicDisposeRacingCodecTeardownClosesManagedStreamExactlyOnce() + { + // Ignored public Dispose (no-ops on IgnorePublicDispose) racing the codec's owner-teardown + // (the sole disposer, via DisposeUnownedManaged -> child.DisposeInternal). The underlying + // .NET stream must be closed EXACTLY once — proven by DisposeCount. fromNative is only a + // one-way latch (Interlocked.Exchange), so fromNative==1 asserts the native destroy callback + // was OBSERVED, not that it fired exactly once; DisposeCount carries the exactly-once proof. + const int iterations = 200; + var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); + + for (var i = 0; i < iterations; i++) + { + var dotnet = new GatedCountingStream(bytes); + var stream = new SKManagedStream(dotnet, true); + var handle = stream.Handle; + var codec = SKCodec.Create(stream); + try + { + Assert.False(stream.OwnsHandle); + Assert.True(stream.IgnorePublicDispose); + + using (var barrier = new Barrier(2)) + { + var teardownThreadId = 0; + // Dedicated threads (not Task.Run) so the Barrier(2) always has both participants. + Exception disposeError = null, teardownError = null; + var dispose = new Thread (() => { try { barrier.SignalAndWait (); stream.Dispose (); } catch (Exception ex) { disposeError = ex; } }) { IsBackground = true }; + var teardown = new Thread (() => { try { barrier.SignalAndWait (); teardownThreadId = Environment.CurrentManagedThreadId; codec.Dispose (); } catch (Exception ex) { teardownError = ex; } }) { IsBackground = true }; + dispose.Start (); + teardown.Start (); + var completed = dispose.Join (TimeSpan.FromSeconds (30)); + completed &= teardown.Join (TimeSpan.FromSeconds (30)); + Assert.True (completed, "Dispose/teardown race did not complete in time."); + if (disposeError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (disposeError); + if (teardownError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (teardownError); + + // The ignored public Dispose never closes the stream; the codec owner-teardown is the + // real disposer. Proven by the closing thread being the teardown task, not the public + // Dispose task — DisposeCount==1 alone could not distinguish which path won. + Assert.Equal (teardownThreadId, dotnet.FirstDisposeThreadId); + } + } + finally + { + // Idempotent: redundant if the racing thread already tore the codec down. + codec.Dispose(); + } + + // Ignored public Dispose + single owner teardown => one .NET stream close, one latch flip. + Assert.Equal(1, dotnet.DisposeCount); + Assert.Equal(1, stream.fromNative); + Assert.True(stream.IsDisposed); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + } + + [SkippableFact] + public unsafe void NonSeekableStreamReparentedToCodecTearsDownNestedStreamExactlyOnce() + { + // A NON-SEEKABLE stream routes SKCodec.Create through SKFrontBufferedManagedStream, which + // holds a private *inner* SKManagedStream wrapping the user's .NET stream. Only the OUTER + // front-buffered wrapper is reparented onto the codec (OwnsHandle=false, IgnorePublicDispose + // =true); the inner SKManagedStream is UNTOUCHED (still owns its handle, still forwards public + // Dispose). The user's racing fb.Dispose() must be a no-op, and codec teardown must walk + // fb -> inner -> .NET stream and close the .NET stream EXACTLY once. We supply the inner + // explicitly (public ctor) so both wrappers' deregistration is asserted without reflection. + var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); + + var dotnet = new NonSeekableGatedCountingStream(bytes); + var inner = new SKManagedStream(dotnet, true); + var innerHandle = inner.Handle; + var fb = new SKFrontBufferedManagedStream(inner, SKCodec.MinBufferedBytesNeeded, true); + var fbHandle = fb.Handle; + var codec = SKCodec.Create(fb); + try + { + Assert.NotNull(codec); + + // Only the outer front-buffered wrapper is reparented; the inner stays a normal owner. + Assert.False(fb.OwnsHandle); + Assert.True(fb.IgnorePublicDispose); + Assert.True(inner.OwnsHandle); + Assert.False(inner.IgnorePublicDispose); + + Assert.Equal(SKCodecResult.Success, codec.GetPixels(out _)); + + // The user still holds fb and (wrongly) disposes it: it is reparented, so this is a no-op. + fb.Dispose(); + Assert.False(fb.IsDisposed); + Assert.False(inner.IsDisposed); + Assert.Equal(0, dotnet.DisposeCount); + } + finally + { + codec.Dispose(); + } + + // Codec teardown is the sole disposer: fb -> inner -> .NET stream, closed exactly once. + Assert.True(fb.IsDisposed); + Assert.True(inner.IsDisposed); + Assert.Equal(1, dotnet.DisposeCount); + SKHandleDictionaryTestHelpers.AssertDeregistered(fbHandle, fb); + SKHandleDictionaryTestHelpers.AssertDeregistered(innerHandle, inner); + } + + [SkippableFact] + public unsafe void PublicDisposeWhileFrontBufferedCodecReadIsInFlightIsIgnored() + { + // DETERMINISTIC in-flight-read race for the NESTED non-seekable graph. Park the native lazy + // read inside the underlying .NET Stream.Read() (reached through fb's front buffer -> inner + // SKManagedStream), fire the reparented fb's public Dispose() while that read is provably + // in-flight, then release. The ignored public Dispose must not close anything mid-read, so + // GetPixels succeeds and the .NET stream is closed exactly once by codec teardown. + var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); + + using var readEntered = new ManualResetEventSlim(false); + using var releaseRead = new ManualResetEventSlim(false); + + var dotnet = new NonSeekableGatedCountingStream(bytes); + var fb = new SKFrontBufferedManagedStream(dotnet, SKCodec.MinBufferedBytesNeeded, true); + var fbHandle = fb.Handle; + var codec = SKCodec.Create(fb); + Task reader = null; + try + { + Assert.False(fb.OwnsHandle); + Assert.True(fb.IgnorePublicDispose); + + // Arm only AFTER Create() so the gate trips on a GetPixels read past the front buffer, not + // on the header bytes buffered during format detection. + dotnet.Arm(readEntered, releaseRead); + + var result = SKCodecResult.InternalError; + reader = Task.Run(() => result = codec.GetPixels(out _)); + + Assert.True(readEntered.Wait(TimeSpan.FromSeconds(30)), "Native lazy read never reached the underlying non-seekable stream."); + + // The native read is parked inside the underlying Read(); a public Dispose now must no-op. + fb.Dispose(); + Assert.False(fb.IsDisposed); + Assert.Equal(0, dotnet.DisposeCount); + + releaseRead.Set(); + Assert.True(reader.Wait(TimeSpan.FromSeconds(30)), "GetPixels did not complete after the read was released."); + + Assert.Equal(SKCodecResult.Success, result); + Assert.False(fb.IsDisposed); + Assert.Equal(0, dotnet.DisposeCount); + } + finally + { + releaseRead.Set(); + try { reader?.Wait(TimeSpan.FromSeconds(30)); } catch { } + codec.Dispose(); + } + + // The codec is the sole disposer; the underlying .NET stream was closed exactly once. + Assert.True(fb.IsDisposed); + Assert.Equal(1, dotnet.DisposeCount); + SKHandleDictionaryTestHelpers.AssertDeregistered(fbHandle, fb); + } + + [SkippableFact] + public unsafe void InvalidNonSeekableStreamFailedCodecCreateClosesNestedStreamExactlyOnce() + { + // FAILURE path for the nested non-seekable graph: when the codec cannot be created, Create + // still transfers ownership to a null native object, which disposes the front-buffered + // wrapper immediately. That teardown must walk fb -> inner -> .NET stream and close it + // exactly once, and both wrappers must deregister — no leak, no double-free. + var bytes = new byte[256]; + for (var i = 0; i < bytes.Length; i++) + bytes[i] = (byte)(i + 1); + + var dotnet = new NonSeekableGatedCountingStream(bytes); + var inner = new SKManagedStream(dotnet, true); + var innerHandle = inner.Handle; + var fb = new SKFrontBufferedManagedStream(inner, SKCodec.MinBufferedBytesNeeded, true); + var fbHandle = fb.Handle; + + var codec = SKCodec.Create(fb, out var result); + + Assert.Null(codec); + Assert.NotEqual(SKCodecResult.Success, result); + + // The failed Create disposed fb immediately, which tore down the whole nested chain once. + Assert.True(fb.IsDisposed); + Assert.True(inner.IsDisposed); + Assert.Equal(1, dotnet.DisposeCount); + SKHandleDictionaryTestHelpers.AssertDeregistered(fbHandle, fb); + SKHandleDictionaryTestHelpers.AssertDeregistered(innerHandle, inner); + } + + [SkippableFact] + public unsafe void InvalidSeekableManagedStreamFailedCodecCreateClosesStreamExactlyOnce() + { + // FAILURE path for the FLAT seekable graph (no front-buffering): a seekable managed Stream + // of invalid bytes is wrapped in a single SKManagedStream. When the codec cannot be created, + // Create transfers ownership to a null native object, so RevokeOwnership(null) disposes the + // wrapper immediately. That teardown must close the .NET stream EXACTLY once and deregister + // the wrapper — no leak, no double-free. Complements the non-seekable/front-buffered failure + // test above, which exercises a different (nested) teardown graph. + var bytes = new byte[256]; + for (var i = 0; i < bytes.Length; i++) + bytes[i] = (byte)(i + 1); + + var dotnet = new GatedCountingStream(bytes); + var stream = new SKManagedStream(dotnet, true); + var handle = stream.Handle; + + Assert.True(stream.OwnsHandle); + Assert.True(SKObject.GetInstance(handle, out _)); + + var codec = SKCodec.Create(stream, out var result); + + Assert.Null(codec); + Assert.NotEqual(SKCodecResult.Success, result); + + // Failed Create disposed the flat wrapper, closing the .NET stream exactly once. + Assert.True(stream.IsDisposed); + Assert.Equal(1, dotnet.DisposeCount); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + + [SkippableFact] + public unsafe void FrontBufferedCodecWithoutOwnershipDoesNotCloseUnderlyingStream() + { + // disposeUnderlyingStream:false — the user keeps ownership of the .NET stream. The nested + // SKManagedStream is still disposed by codec teardown, but it must NOT close the user's .NET + // stream. We then close it ourselves to prove the count is driven solely by the user, not the + // codec chain. + var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); + + var dotnet = new NonSeekableGatedCountingStream(bytes); + var fb = new SKFrontBufferedManagedStream(dotnet, SKCodec.MinBufferedBytesNeeded, false); + var fbHandle = fb.Handle; + var codec = SKCodec.Create(fb); + try + { + Assert.NotNull(codec); + Assert.False(fb.OwnsHandle); + Assert.True(fb.IgnorePublicDispose); + Assert.Equal(SKCodecResult.Success, codec.GetPixels(out _)); + } + finally + { + codec.Dispose(); + } + + // Codec teardown disposed the wrappers but, lacking ownership, left the .NET stream open. + Assert.True(fb.IsDisposed); + SKHandleDictionaryTestHelpers.AssertDeregistered(fbHandle, fb); + Assert.Equal(0, dotnet.DisposeCount); + + // The user is the only owner: closing now is the first and only close. + dotnet.Dispose(); + Assert.Equal(1, dotnet.DisposeCount); + } + + // A seekable .NET Stream over a byte buffer that (a) counts how many times it is disposed and + // (b) can park the FIRST read after Arm() until released — used to drive a deterministic + // "public Dispose while the native lazy read is in-flight" race and to count exact teardown. + private sealed class GatedCountingStream : Stream + { + private readonly MemoryStream inner; + private ManualResetEventSlim readEntered; + private ManualResetEventSlim releaseRead; + private int gateArmed; + private int disposeCount; + private int firstDisposeThreadId; + + public GatedCountingStream(byte[] bytes) => inner = new MemoryStream(bytes, writable: false); + + public int DisposeCount => Volatile.Read(ref disposeCount); + + // Managed thread id of whoever closed the .NET stream first. Lets a test prove the codec + // owner-teardown — not the ignored public Dispose — was the actual disposer. + public int FirstDisposeThreadId => Volatile.Read(ref firstDisposeThreadId); + + public void Arm(ManualResetEventSlim entered, ManualResetEventSlim release) + { + readEntered = entered; + releaseRead = release; + Volatile.Write(ref gateArmed, 1); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (Interlocked.CompareExchange(ref gateArmed, 0, 1) == 1) + { + readEntered.Set(); + releaseRead.Wait(); + } + return inner.Read(buffer, offset, count); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (Interlocked.Increment(ref disposeCount) == 1) + Volatile.Write(ref firstDisposeThreadId, Environment.CurrentManagedThreadId); + // Actually close the backing buffer: a premature close during an in-flight read + // then surfaces as an ObjectDisposedException from the parked inner.Read. + inner.Dispose(); + } + base.Dispose(disposing); + } + + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => false; + public override long Length => inner.Length; + public override long Position { get => inner.Position; set => inner.Position = value; } + public override long Seek(long offset, SeekOrigin origin) => inner.Seek(offset, origin); + public override void Flush() => inner.Flush(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } + + // A NON-SEEKABLE .NET Stream over a byte buffer that (a) counts how many times it is disposed, + // (b) records the disposing thread id, and (c) can park the first read after Arm() until + // released. Non-seekable so SKCodec.Create routes it through the SKFrontBufferedManagedStream + // (nested SKManagedStream) path — a different teardown graph from the flat seekable wrapper. + private sealed class NonSeekableGatedCountingStream : Stream + { + private readonly MemoryStream inner; + private ManualResetEventSlim readEntered; + private ManualResetEventSlim releaseRead; + private int gateArmed; + private int disposeCount; + private int firstDisposeThreadId; + + public NonSeekableGatedCountingStream(byte[] bytes) => inner = new MemoryStream(bytes, writable: false); + + public int DisposeCount => Volatile.Read(ref disposeCount); + + public int FirstDisposeThreadId => Volatile.Read(ref firstDisposeThreadId); + + public void Arm(ManualResetEventSlim entered, ManualResetEventSlim release) + { + readEntered = entered; + releaseRead = release; + Volatile.Write(ref gateArmed, 1); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (Interlocked.CompareExchange(ref gateArmed, 0, 1) == 1) + { + readEntered.Set(); + releaseRead.Wait(); + } + return inner.Read(buffer, offset, count); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (Interlocked.Increment(ref disposeCount) == 1) + Volatile.Write(ref firstDisposeThreadId, Environment.CurrentManagedThreadId); + inner.Dispose(); + } + base.Dispose(disposing); + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position { get => inner.Position; set => throw new NotSupportedException(); } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void Flush() => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } + + // A MemoryStream that records disposal and rejects (rather than silently swallowing) + // any write that arrives after the stream has been closed — so a premature close in + // the owned-wstream teardown ordering would surface as WroteAfterClose / an exception. + private sealed class LifecycleTrackingStream : Stream + { + private readonly MemoryStream inner = new MemoryStream(); + private bool closed; + + public bool IsDisposed => closed; + public long BytesWritten { get; private set; } + public bool WroteAfterClose { get; private set; } + + public override bool CanRead => !closed && inner.CanRead; + public override bool CanSeek => !closed && inner.CanSeek; + public override bool CanWrite => !closed && inner.CanWrite; + public override long Length => inner.Length; + + public override long Position + { + get => inner.Position; + set => inner.Position = value; + } + + public override void Flush() => inner.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + inner.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => + inner.Seek(offset, origin); + + public override void SetLength(long value) => inner.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) + { + if (closed) + { + WroteAfterClose = true; + throw new ObjectDisposedException(nameof(LifecycleTrackingStream)); + } + + BytesWritten += count; + inner.Write(buffer, offset, count); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + closed = true; + + // keep the underlying buffer readable for post-dispose assertions + base.Dispose(disposing); + } + } } } diff --git a/tests/Tests/SkiaSharp/SKTypefaceTest.cs b/tests/Tests/SkiaSharp/SKTypefaceTest.cs index 2e37c3a2232..67ed0ad6580 100644 --- a/tests/Tests/SkiaSharp/SKTypefaceTest.cs +++ b/tests/Tests/SkiaSharp/SKTypefaceTest.cs @@ -329,7 +329,11 @@ public unsafe void InvalidStreamIsDisposedImmediately() Assert.Null(SKTypeface.FromStream(stream)); Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); + // Failed FromStream runs RevokeOwnership(null) -> DisposeInternal(): the wrapper is genuinely + // disposed (and deregistered, asserted below), NOT dispose-protected. (ff18b0f's blanket + // IsDisposed->IgnorePublicDispose rename was correct for the success/reparent path but wrong + // for this failure path, where there is no new owner to protect the wrapper for.) + Assert.True(stream.IsDisposed); Assert.False(SKObject.GetInstance(handle, out _)); } @@ -406,7 +410,10 @@ public unsafe void ManagedStreamIsCollectedWhenTypefaceIsDisposed() typeface.Dispose(); - Assert.False(SKObject.GetInstance(handle, out _)); + // Address-reuse safe: under xUnit's parallel collections a parallel test can + // reallocate a fresh wrapper at this freed native pointer, so asserting the + // address is simply absent is racy. Assert by reference identity instead. + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); Assert.Throws(() => dotnet.Position); } From 535048ab020f52c1cc87f572e31fd7b9ec51f2a1 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Wed, 3 Jun 2026 01:13:01 +0200 Subject: [PATCH 17/29] Add re-entrancy contract tests for HandleDictionary.GetOrAddObject Pin the platform-divergent re-entrancy behaviour of GetOrAddObject, whose factory runs inside the upgradeable-read lock: - A factory MAY read the dictionary (GetInstance) from inside the lock; this is a legal upgradeable->read downgrade and works on every platform. - A factory MUST NOT create another registered object (nested GetOrAddObject): on non-Windows the lock is ReaderWriterLockSlim(NoRecursion) and throws LockRecursionException; on Windows the recursive CRITICAL_SECTION (#1383) does not throw. The non-Windows assertion is the only one exercised here. Both behaviours were verified empirically before the asserts were written. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SKHandleDictionaryReentrancyTest.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs new file mode 100644 index 00000000000..489adaca0a8 --- /dev/null +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs @@ -0,0 +1,62 @@ +using System.Threading; +using Xunit; + +namespace SkiaSharp.Tests +{ +// Pins the re-entrancy contract of HandleDictionary.GetOrAddObject, which runs the object +// factory INSIDE the upgradeable-read lock. The allowed/forbidden re-entrant operations are a +// direct consequence of PlatformLock and are platform-divergent, so they are easy to break +// accidentally in a future lock refactor. These tests lock the observed behaviour down. +// +// Verified empirically (macOS) before being written: +// reentrant GetInstance (read) -> succeeds +// nested GetOrAddObject (create) -> throws LockRecursionException (non-Windows) +public class SKHandleDictionaryReentrancyTest : SKTest +{ +// A factory MAY read the dictionary. GetInstance takes a read lock, and a thread already +// holding the upgradeable-read lock is permitted to enter read mode under +// ReaderWriterLockSlim(NoRecursion) (a legal upgradeable->read downgrade) and may also +// recurse on the Windows CRITICAL_SECTION. So this is safe on every platform. +[SkippableFact] +public void FactoryMayReadDictionaryFromWithinTheLock () +{ +var probe = SKHandleDictionaryTestHelpers.NextHandle (); +var handle = SKHandleDictionaryTestHelpers.NextHandle (); + +using var obj = HandleDictionary.GetOrAddObject (handle, false, true, (h, owns) => { +// Must not throw: a read from inside the factory is part of the supported contract. +HandleDictionary.GetInstance (probe, out _); +return new FakeNativeObject (h); +}); + +Assert.NotNull (obj); +Assert.True (HandleDictionary.GetInstance (handle, out var found)); +Assert.Same (obj, found); +} + +// A factory MUST NOT create another registered object: a nested GetOrAddObject is an +// upgradeable->upgradeable re-acquire, i.e. recursion. On non-Windows the lock is +// ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion) and throws LockRecursionException; +// on Windows the lock is a recursive Win32 CRITICAL_SECTION (the #1383 STA-pump fix) and it +// would NOT throw. We assert only the platform we can run here; the throw aborts the lock +// re-acquire before any wrapper is constructed, so nothing leaks into the registry. +[SkippableFact] +public void FactoryCannotCreateNestedRegisteredObjectOnNonWindows () +{ +Skip.If (IsWindows, "Windows uses a recursive CRITICAL_SECTION (#1383); nested creation does not throw there."); + +var nested = SKHandleDictionaryTestHelpers.NextHandle (); +var outer = SKHandleDictionaryTestHelpers.NextHandle (); + +Assert.Throws (() => { +HandleDictionary.GetOrAddObject (outer, false, true, (h, owns) => +HandleDictionary.GetOrAddObject (nested, false, true, (h2, o2) => new FakeNativeObject (h2))); +}); + +// The nested re-acquire threw before constructing either wrapper, so neither handle +// was ever registered. (No address-reuse concern: these are synthetic unique handles.) +Assert.False (HandleDictionary.GetInstance (outer, out _)); +Assert.False (HandleDictionary.GetInstance (nested, out _)); +} +} +} From 3ff3e5caa0a7530c4bf9dfa0fadc88cba70e4c42 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Wed, 3 Jun 2026 01:43:53 +0200 Subject: [PATCH 18/29] Guard AssemblyLoadContext singleton test for .NET Framework The isolated AssemblyLoadContext cold-start regression test only works on .NET Core, but the main test project also targets net48 on Windows CI. Wrap the ALC-specific test and its System.Runtime.Loader using directive in #if !NETFRAMEWORK so the remaining singleton tests still compile and run on .NET Framework. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/Tests/SkiaSharp/SKSingletonInitTest.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Tests/SkiaSharp/SKSingletonInitTest.cs b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs index 0c251a79c29..c5ac595077d 100644 --- a/tests/Tests/SkiaSharp/SKSingletonInitTest.cs +++ b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs @@ -1,6 +1,8 @@ using System; using System.Reflection; +#if !NETFRAMEWORK using System.Runtime.Loader; +#endif using System.Threading; using System.Threading.Tasks; using Xunit; @@ -143,6 +145,7 @@ public void SKPaintConstructionDoesNotThrow() Assert.NotNull(paint); } +#if !NETFRAMEWORK // --- #3817 cold-start cctor cycle (best-effort) --- // When SKFontManager.Default is the FIRST SkiaSharp managed type touched @@ -212,5 +215,6 @@ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) return path != null ? LoadUnmanagedDllFromPath(path) : IntPtr.Zero; } } +#endif } } From 005db38403d6529e776ead5999b4f8aba0854cfc Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Wed, 3 Jun 2026 01:47:57 +0200 Subject: [PATCH 19/29] Fix indentation in HandleDictionary re-entrancy test The file was committed without indentation. Re-indent with tabs to match the rest of the test suite. Whitespace-only change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SKHandleDictionaryReentrancyTest.cs | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs index 489adaca0a8..99f8062d929 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs @@ -3,60 +3,60 @@ namespace SkiaSharp.Tests { -// Pins the re-entrancy contract of HandleDictionary.GetOrAddObject, which runs the object -// factory INSIDE the upgradeable-read lock. The allowed/forbidden re-entrant operations are a -// direct consequence of PlatformLock and are platform-divergent, so they are easy to break -// accidentally in a future lock refactor. These tests lock the observed behaviour down. -// -// Verified empirically (macOS) before being written: -// reentrant GetInstance (read) -> succeeds -// nested GetOrAddObject (create) -> throws LockRecursionException (non-Windows) -public class SKHandleDictionaryReentrancyTest : SKTest -{ -// A factory MAY read the dictionary. GetInstance takes a read lock, and a thread already -// holding the upgradeable-read lock is permitted to enter read mode under -// ReaderWriterLockSlim(NoRecursion) (a legal upgradeable->read downgrade) and may also -// recurse on the Windows CRITICAL_SECTION. So this is safe on every platform. -[SkippableFact] -public void FactoryMayReadDictionaryFromWithinTheLock () -{ -var probe = SKHandleDictionaryTestHelpers.NextHandle (); -var handle = SKHandleDictionaryTestHelpers.NextHandle (); + // Pins the re-entrancy contract of HandleDictionary.GetOrAddObject, which runs the object + // factory INSIDE the upgradeable-read lock. The allowed/forbidden re-entrant operations are a + // direct consequence of PlatformLock and are platform-divergent, so they are easy to break + // accidentally in a future lock refactor. These tests lock the observed behaviour down. + // + // Verified empirically (macOS) before being written: + // reentrant GetInstance (read) -> succeeds + // nested GetOrAddObject (create) -> throws LockRecursionException (non-Windows) + public class SKHandleDictionaryReentrancyTest : SKTest + { + // A factory MAY read the dictionary. GetInstance takes a read lock, and a thread already + // holding the upgradeable-read lock is permitted to enter read mode under + // ReaderWriterLockSlim(NoRecursion) (a legal upgradeable->read downgrade) and may also + // recurse on the Windows CRITICAL_SECTION. So this is safe on every platform. + [SkippableFact] + public void FactoryMayReadDictionaryFromWithinTheLock () + { + var probe = SKHandleDictionaryTestHelpers.NextHandle (); + var handle = SKHandleDictionaryTestHelpers.NextHandle (); -using var obj = HandleDictionary.GetOrAddObject (handle, false, true, (h, owns) => { -// Must not throw: a read from inside the factory is part of the supported contract. -HandleDictionary.GetInstance (probe, out _); -return new FakeNativeObject (h); -}); + using var obj = HandleDictionary.GetOrAddObject (handle, false, true, (h, owns) => { + // Must not throw: a read from inside the factory is part of the supported contract. + HandleDictionary.GetInstance (probe, out _); + return new FakeNativeObject (h); + }); -Assert.NotNull (obj); -Assert.True (HandleDictionary.GetInstance (handle, out var found)); -Assert.Same (obj, found); -} + Assert.NotNull (obj); + Assert.True (HandleDictionary.GetInstance (handle, out var found)); + Assert.Same (obj, found); + } -// A factory MUST NOT create another registered object: a nested GetOrAddObject is an -// upgradeable->upgradeable re-acquire, i.e. recursion. On non-Windows the lock is -// ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion) and throws LockRecursionException; -// on Windows the lock is a recursive Win32 CRITICAL_SECTION (the #1383 STA-pump fix) and it -// would NOT throw. We assert only the platform we can run here; the throw aborts the lock -// re-acquire before any wrapper is constructed, so nothing leaks into the registry. -[SkippableFact] -public void FactoryCannotCreateNestedRegisteredObjectOnNonWindows () -{ -Skip.If (IsWindows, "Windows uses a recursive CRITICAL_SECTION (#1383); nested creation does not throw there."); + // A factory MUST NOT create another registered object: a nested GetOrAddObject is an + // upgradeable->upgradeable re-acquire, i.e. recursion. On non-Windows the lock is + // ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion) and throws LockRecursionException; + // on Windows the lock is a recursive Win32 CRITICAL_SECTION (the #1383 STA-pump fix) and it + // would NOT throw. We assert only the platform we can run here; the throw aborts the lock + // re-acquire before any wrapper is constructed, so nothing leaks into the registry. + [SkippableFact] + public void FactoryCannotCreateNestedRegisteredObjectOnNonWindows () + { + Skip.If (IsWindows, "Windows uses a recursive CRITICAL_SECTION (#1383); nested creation does not throw there."); -var nested = SKHandleDictionaryTestHelpers.NextHandle (); -var outer = SKHandleDictionaryTestHelpers.NextHandle (); + var nested = SKHandleDictionaryTestHelpers.NextHandle (); + var outer = SKHandleDictionaryTestHelpers.NextHandle (); -Assert.Throws (() => { -HandleDictionary.GetOrAddObject (outer, false, true, (h, owns) => -HandleDictionary.GetOrAddObject (nested, false, true, (h2, o2) => new FakeNativeObject (h2))); -}); + Assert.Throws (() => { + HandleDictionary.GetOrAddObject (outer, false, true, (h, owns) => + HandleDictionary.GetOrAddObject (nested, false, true, (h2, o2) => new FakeNativeObject (h2))); + }); -// The nested re-acquire threw before constructing either wrapper, so neither handle -// was ever registered. (No address-reuse concern: these are synthetic unique handles.) -Assert.False (HandleDictionary.GetInstance (outer, out _)); -Assert.False (HandleDictionary.GetInstance (nested, out _)); -} -} + // The nested re-acquire threw before constructing either wrapper, so neither handle + // was ever registered. (No address-reuse concern: these are synthetic unique handles.) + Assert.False (HandleDictionary.GetInstance (outer, out _)); + Assert.False (HandleDictionary.GetInstance (nested, out _)); + } + } } From 8809e45905d9f3eaa7b01ea1926fad5b50afd6a4 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Wed, 3 Jun 2026 02:39:54 +0200 Subject: [PATCH 20/29] Guard concurrency tests for single-threaded WASM and 32-bit targets Add IsBrowser skips to tests that need real OS threads or use APIs unsupported on browser WASM (custom AssemblyLoadContext). Make the synthetic handle seed 32-bit-safe so x86 Windows/Linux and WASM no longer hit OverflowException in new IntPtr(long). Root both wrappers across the thread-id assertion in the racing-teardown stream test so a stray finalizer cannot flip FirstDisposeThreadId mid-assert. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SKHandleDictionaryDisposeProtectedTest.cs | 2 ++ .../SKHandleDictionaryReservationTest.cs | 26 ++++++++++++++++--- .../SKHandleDictionaryStaleWrapperTest.cs | 6 +++++ .../SKHandleDictionaryTestHelpers.cs | 6 ++++- tests/Tests/SkiaSharp/SKManagedStreamTest.cs | 17 ++++++++++++ .../SkiaSharp/SKSingletonConcurrencyTest.cs | 18 +++++++++---- tests/Tests/SkiaSharp/SKSingletonInitTest.cs | 23 ++-------------- 7 files changed, 68 insertions(+), 30 deletions(-) diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs index 80846d559e0..c9ff1fc4d18 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs @@ -51,6 +51,8 @@ public void DisposeProtectedFreshConstructionSetsFlag () [SkippableFact] public void DisposeProtectedRacingPublicDisposeNeverTears () { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + const int iterations = 300; var created = new System.Collections.Concurrent.ConcurrentBag (); diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs index 015171592c8..3fe17c089e7 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs @@ -24,11 +24,18 @@ namespace SkiaSharp.Tests // would not — so any such test is platform-dependent and is omitted by design. public class SKHandleDictionaryReservationTest : SKTest { - // --- Exactly-once construction under concurrency --- + // --- Exactly-once construction under contention --- + // + // The upgradeable-read lock serializes the contending callers, so factoryCalls == 1 + // is the guaranteed outcome of the lock-the-whole-factory model rather than a race + // the test wins. The value is a regression guard: narrowing the lock scope so two + // callers could run the factory concurrently for the same handle would make this fail. [SkippableFact] public void ConcurrentSameHandleConstructsExactlyOnce () { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + var handle = NextHandle (); var factoryCalls = 0; @@ -60,6 +67,8 @@ public void ConcurrentSameHandleConstructsExactlyOnce () [SkippableFact] public void WaitersOnlyObserveFullyConstructedWrapper () { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + var handle = NextHandle (); const int threadCount = 16; @@ -88,7 +97,11 @@ public void WaitersOnlyObserveFullyConstructedWrapper () for (var i = 0; i < threadCount; i++) { Assert.NotNull (results[i]); Assert.Same (results[0], results[i]); - // If a waiter ever saw the object before its ctor finished, this would be 0. + // Under the lock-the-whole-factory model a waiter can only obtain the wrapper + // through GetOrAddObject's return value, i.e. after the ctor has run, so this + // is 1 by construction. The assert is a regression guard: if the lock scope + // were ever narrowed to publish the entry before the factory completed, a + // waiter could observe FullyConstructed == 0 here. Assert.Equal (1, results[i].FullyConstructed); } } finally { @@ -125,11 +138,18 @@ public void FactoryFailureLeavesNothingRegisteredAndAllowsReconstruction () } } - // --- Concurrent storm where the FIRST owner's factory fails: a waiter recovers --- + // --- The FIRST owner's factory fails; a later caller recovers --- + // + // The callers contend, but the upgradeable-read lock serializes them, so this reduces + // to "first caller throws under the lock -> registry left empty -> next caller in line + // reconstructs". It pins that a throwing factory leaves no stranded half-state for the + // callers queued behind it. [SkippableFact] public void ConcurrentFactoryFailureRecoveredByWaiter () { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + var handle = NextHandle (); var failFirst = 1; var successfulFactoryCalls = 0; diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs index a756a414a0f..d2df9b95e29 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs @@ -66,6 +66,8 @@ public void AddressReuseAfterDisposeConstructsFreshWrapper () [SkippableFact] public void StaleWrapperCleanupDoesNotEvictGetOrAddReplacementForReusedHandle () { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + var handle = NextHandle (); using var enteredCleanup = new ManualResetEventSlim (false); @@ -126,6 +128,8 @@ public void StaleWrapperCleanupDoesNotEvictGetOrAddReplacementForReusedHandle () [SkippableFact] public void StaleWrapperCleanupDoesNotEvictDirectCtorReplacementForReusedHandle () { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + var handle = NextHandle (); using var enteredCleanup = new ManualResetEventSlim (false); @@ -179,6 +183,8 @@ public void StaleWrapperCleanupDoesNotEvictDirectCtorReplacementForReusedHandle [SkippableFact] public void TwoStaleWrappersDeregisteringDoNotEvictThirdReplacementForReusedHandle () { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + var handle = NextHandle (); using var entered1 = new ManualResetEventSlim (false); diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs index 48dd2b7e18f..dfcf5567e04 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs @@ -78,7 +78,11 @@ protected override void DisposeManaged () internal static class SKHandleDictionaryTestHelpers { - private static long handleSeed = 0x6000_0000_0000_0000L; + // Synthetic-handle seed kept well away from any real native pointer range. On 64-bit targets + // that is a high 64-bit value. On 32-bit targets (x86 Windows, x86 Linux, browser WASM) IntPtr + // is 32-bit, so a 64-bit seed would overflow new IntPtr(long); use a high 32-bit value there. + // 0x4000_0000 fits in Int32 and leaves room for many +0x1000 increments before overflow. + private static long handleSeed = IntPtr.Size == 4 ? 0x4000_0000L : 0x6000_0000_0000_0000L; // Hands out unique synthetic handles well away from any real native pointer range. public static IntPtr NextHandle () => diff --git a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs index b96ab14fce2..39ab1a33357 100644 --- a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs +++ b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs @@ -862,6 +862,8 @@ public void DisposingManagedStreamTwiceIsNoOp() [SkippableFact] public void ConcurrentDisposeOfSameManagedStreamIsIdempotent() { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + // Many threads racing Dispose() on the SAME wrapper must funnel through the single // isDisposed CAS: one cleanup, native freed once, destroy callback flips fromNative once. var stream = new SKManagedStream(CreateTestStream(), true); @@ -879,6 +881,8 @@ public void ConcurrentDisposeOfSameManagedStreamIsIdempotent() [SkippableFact] public void ConcurrentDisposeOfManyManagedStreamsIsSafe() { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + const int count = 128; var streams = new List(count); for (var i = 0; i < count; i++) @@ -908,6 +912,8 @@ public void ConcurrentDisposeOfManyManagedStreamsIsSafe() [SkippableFact] public unsafe void PublicDisposeWhileCodecReadIsInFlightIsIgnored() { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + // DETERMINISTIC version of the race: park the native lazy read INSIDE the .NET // Stream.Read() callback, fire the (reparented) wrapper's public Dispose() while the // native read is provably in-flight, then release the read. The public Dispose must be @@ -968,6 +974,8 @@ public unsafe void PublicDisposeWhileCodecReadIsInFlightIsIgnored() [SkippableFact] public unsafe void PublicDisposeAroundLazyCodecReadIsIgnoredStress() { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + // STRESS version: barrier-synchronised public Dispose vs a full GetPixels, repeated many // times to shake out interleavings beyond the single overlap the deterministic gate pins. // (The barrier only co-starts the two operations; it does NOT guarantee Dispose overlaps an @@ -1022,6 +1030,8 @@ public unsafe void PublicDisposeAroundLazyCodecReadIsIgnoredStress() [SkippableFact] public unsafe void PublicDisposeRacingCodecTeardownClosesManagedStreamExactlyOnce() { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + // Ignored public Dispose (no-ops on IgnorePublicDispose) racing the codec's owner-teardown // (the sole disposer, via DisposeUnownedManaged -> child.DisposeInternal). The underlying // .NET stream must be closed EXACTLY once — proven by DisposeCount. fromNative is only a @@ -1060,6 +1070,11 @@ public unsafe void PublicDisposeRacingCodecTeardownClosesManagedStreamExactlyOnc // real disposer. Proven by the closing thread being the teardown task, not the public // Dispose task — DisposeCount==1 alone could not distinguish which path won. Assert.Equal (teardownThreadId, dotnet.FirstDisposeThreadId); + + // Keep both wrappers rooted across the race assertion so a GC + finalizer + // can never flip FirstDisposeThreadId from the finalizer thread mid-assert. + GC.KeepAlive (stream); + GC.KeepAlive (dotnet); } } finally @@ -1128,6 +1143,8 @@ public unsafe void NonSeekableStreamReparentedToCodecTearsDownNestedStreamExactl [SkippableFact] public unsafe void PublicDisposeWhileFrontBufferedCodecReadIsInFlightIsIgnored() { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + // DETERMINISTIC in-flight-read race for the NESTED non-seekable graph. Park the native lazy // read inside the underlying .NET Stream.Read() (reached through fb's front buffer -> inner // SKManagedStream), fire the reparented fb's public Dispose() while that read is provably diff --git a/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs b/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs index b2b5e8780d4..496b02095f6 100644 --- a/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs +++ b/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs @@ -10,12 +10,18 @@ public class SKSingletonConcurrencyTest : SKTest { // PROBABILISTIC deadlock canary (NOT a deterministic race proof). // - // SKTypeface.Default's factory acquires several locks in order: + // Every thread touches the same set of singleton accessors. Each accessor's factory + // transitively acquires several locks, e.g. SKTypeface.Default: // defaultTypefaceLock -> SKFontManager.Default lock -> SKFontStyle cctor -> HD lock. - // No other test exercises this multi-lock acquisition path (the author's - // concurrent test only hammers CreateSrgb, a single-lock path). If a future - // change introduces a reverse acquisition order, 32 threads slamming every - // accessor at a barrier will very likely deadlock here and hang the run. + // No other test exercises this multi-lock path (the author's concurrent test only + // hammers CreateSrgb, a single-lock path). Because the threads run unsynchronized + // after the barrier, at any instant they sit at different accessors and therefore + // hold different locks. If the PRODUCT code ever acquires the same pair of locks in + // opposite orders across two of these factories (a lock-order inversion in our code, + // not one the test manufactures), this contention can surface it as a hang. The test + // cannot create an inversion the product does not have, so a clean run is necessary + // but not sufficient evidence. + // // The Assert.Same checks are the deterministic part: a correct singleton must // always return one instance regardless of interleaving. // @@ -28,6 +34,8 @@ public class SKSingletonConcurrencyTest : SKTest [SkippableFact] public void AllSingletonAccessorsAreStableUnderContention() { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + const int threadCount = 32; using var barrier = new Barrier(threadCount); diff --git a/tests/Tests/SkiaSharp/SKSingletonInitTest.cs b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs index c5ac595077d..8c523ff5815 100644 --- a/tests/Tests/SkiaSharp/SKSingletonInitTest.cs +++ b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs @@ -3,8 +3,6 @@ #if !NETFRAMEWORK using System.Runtime.Loader; #endif -using System.Threading; -using System.Threading.Tasks; using Xunit; namespace SkiaSharp.Tests @@ -100,25 +98,6 @@ public void DisposeOnDisposeProtectedSingletonIsNoOp() Assert.Same(srgb, SKColorSpace.CreateSrgb()); } - // --- Concurrent init smoke test --- - - [SkippableFact] - public void ConcurrentSingletonAccessReturnsSameInstance() - { - const int threadCount = 32; - using var barrier = new Barrier(threadCount); - var results = new SKColorSpace[threadCount]; - - Parallel.For(0, threadCount, i => - { - barrier.SignalAndWait(); - results[i] = SKColorSpace.CreateSrgb(); - }); - - for (int i = 1; i < threadCount; i++) - Assert.Same(results[0], results[i]); - } - // --- SKTypeface.CreateDefault never returns null even on a cold backing field --- [SkippableFact] @@ -167,6 +146,8 @@ public void SKPaintConstructionDoesNotThrow() [SkippableFact] public void Issue3817_SKFontManagerDefaultDoesNotThrowFromColdStart() { + SkipOnPlatform(IsBrowser, "Browser WASM does not support custom AssemblyLoadContext / AssemblyDependencyResolver"); + var alc = new IsolatedSkiaSharpLoadContext(typeof(SKFontManager).Assembly); try { From 4a4b604fff2c5a18b3a143eed478d91791ba510d Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Wed, 3 Jun 2026 02:57:49 +0200 Subject: [PATCH 21/29] Harden concurrency test joins and cold-start rethrow Route AllSingletonAccessorsAreStableUnderContention through the RunConcurrent helper so workers are background threads joined against a shared deadline: a product-side lock-order inversion now fails the test instead of hanging the suite, and a hung worker cannot keep the host process alive. Replace 'throw ex.InnerException;' in the ALC cold-start test with ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw() to preserve the original stack trace (the test's whole purpose is to show where a cctor cycle throws) and avoid an NRE when InnerException is null. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SkiaSharp/SKSingletonConcurrencyTest.cs | 53 ++++++------------- tests/Tests/SkiaSharp/SKSingletonInitTest.cs | 8 ++- 2 files changed, 22 insertions(+), 39 deletions(-) diff --git a/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs b/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs index 496b02095f6..b5aaf436627 100644 --- a/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs +++ b/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Concurrent; -using System.Threading; using Xunit; namespace SkiaSharp.Tests @@ -25,19 +23,18 @@ public class SKSingletonConcurrencyTest : SKTest // The Assert.Same checks are the deterministic part: a correct singleton must // always return one instance regardless of interleaving. // - // NOTE: explicit Thread[] is used deliberately instead of Parallel.For. A - // Barrier(N) combined with Parallel.For can deadlock spuriously because - // Parallel.For does not guarantee N concurrent workers — a worker blocked on - // SignalAndWait never runs its remaining assigned iterations, so the barrier - // may never reach N participants. Real threads guarantee the barrier is - // satisfiable, so a hang here means a genuine product-side lock-ordering bug. + // Execution is delegated to RunConcurrent, which runs the body on N dedicated + // background threads released together by a Barrier and joins them against a + // shared deadline. That matters here: if the product code does have a + // lock-order inversion across these factories, the contention surfaces as a + // failed Join (a deterministic test FAILURE) instead of a hung suite, and the + // background threads never keep the host process alive after a hang. [SkippableFact] public void AllSingletonAccessorsAreStableUnderContention() { SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); const int threadCount = 32; - using var barrier = new Barrier(threadCount); var colorSpaces = new SKColorSpace[threadCount]; var colorSpaceLinears = new SKColorSpace[threadCount]; @@ -47,37 +44,17 @@ public void AllSingletonAccessorsAreStableUnderContention() var empties = new SKTypeface[threadCount]; var blenders = new SKBlender[threadCount]; - var errors = new ConcurrentBag(); - var threads = new Thread[threadCount]; - for (var t = 0; t < threadCount; t++) + SKHandleDictionaryTestHelpers.RunConcurrent(threadCount, i => { - var i = t; - threads[i] = new Thread(() => - { - try - { - barrier.SignalAndWait(); - colorSpaces[i] = SKColorSpace.CreateSrgb(); - colorSpaceLinears[i] = SKColorSpace.CreateSrgbLinear(); - datas[i] = SKData.Empty; - fontManagers[i] = SKFontManager.Default; - typefaces[i] = SKTypeface.Default; - empties[i] = SKTypeface.Empty; - blenders[i] = SKBlender.CreateBlendMode(SKBlendMode.SrcOver); - } - catch (Exception ex) - { - errors.Add(ex); - } - }); - } - - foreach (var thread in threads) - thread.Start(); - foreach (var thread in threads) - thread.Join(); + colorSpaces[i] = SKColorSpace.CreateSrgb(); + colorSpaceLinears[i] = SKColorSpace.CreateSrgbLinear(); + datas[i] = SKData.Empty; + fontManagers[i] = SKFontManager.Default; + typefaces[i] = SKTypeface.Default; + empties[i] = SKTypeface.Empty; + blenders[i] = SKBlender.CreateBlendMode(SKBlendMode.SrcOver); + }, deadlockMessage: "Singleton accessors deadlocked under contention (possible product-side lock-order inversion)."); - Assert.Empty(errors); AssertAllSame(colorSpaces); AssertAllSame(colorSpaceLinears); AssertAllSame(datas); diff --git a/tests/Tests/SkiaSharp/SKSingletonInitTest.cs b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs index 8c523ff5815..5cfc03d259b 100644 --- a/tests/Tests/SkiaSharp/SKSingletonInitTest.cs +++ b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using System.Runtime.ExceptionServices; #if !NETFRAMEWORK using System.Runtime.Loader; #endif @@ -163,7 +164,12 @@ public void Issue3817_SKFontManagerDefaultDoesNotThrowFromColdStart() } catch (TargetInvocationException ex) { - throw ex.InnerException; + // Surface the real cold-start failure (the Issue #3817 cctor + // cycle) with its original stack intact, rather than throw + // ex.InnerException; which resets the stack and NREs when + // InnerException is null. + ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); + throw; // unreachable; satisfies definite-assignment of value } Assert.NotNull(value); From 73c4c3b2cfa95b9697efe0938769b745f2b59044 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Wed, 3 Jun 2026 09:00:27 +0200 Subject: [PATCH 22/29] Make concurrency test helpers net48-compatible The ported tests used APIs unavailable on .NET Framework 4.8, breaking the netfx CI leg compile: - Environment.TickCount64 (RunConcurrent join deadline) does not exist on net48; replace with a Stopwatch-based remaining-time computation. - The static ExceptionDispatchInfo.Throw(ex) overload does not exist on net48; replace all uses with Capture(ex).Throw(). Test-only change; no production code touched. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs | 6 +++--- tests/Tests/SkiaSharp/SKManagedStreamTest.cs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs index dfcf5567e04..c396ef498e3 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs @@ -124,10 +124,10 @@ public static void RunConcurrent ( // Attempt to join every worker before asserting so that a single timed-out // thread cannot leave the others unjoined (a worker still holding // instancesLock would otherwise cascade into later-test hangs). - var deadline = Environment.TickCount64 + timeoutMs; + var sw = System.Diagnostics.Stopwatch.StartNew (); var allJoined = true; foreach (var t in threads) { - var remaining = (int) Math.Max (0, deadline - Environment.TickCount64); + var remaining = (int) Math.Max (0, timeoutMs - sw.ElapsedMilliseconds); allJoined &= t.Join (remaining); } @@ -159,7 +159,7 @@ public static void RunWithTimeout ( Assert.True (runner.Join (timeoutMs), deadlockMessage); if (captured != null) - System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (captured); + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (captured).Throw (); } // Asserts that a disposed wrapper has been deregistered from the HandleDictionary. diff --git a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs index 39ab1a33357..c8b420c982b 100644 --- a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs +++ b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs @@ -1006,8 +1006,8 @@ public unsafe void PublicDisposeAroundLazyCodecReadIsIgnoredStress() var completed = read.Join (TimeSpan.FromSeconds (30)); completed &= dispose.Join (TimeSpan.FromSeconds (30)); Assert.True (completed, "Dispose/GetPixels race did not complete in time."); - if (readError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (readError); - if (disposeError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (disposeError); + if (readError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (readError).Throw (); + if (disposeError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (disposeError).Throw (); } // The lazy native read saw a live stream; the racing public Dispose was a no-op. @@ -1063,8 +1063,8 @@ public unsafe void PublicDisposeRacingCodecTeardownClosesManagedStreamExactlyOnc var completed = dispose.Join (TimeSpan.FromSeconds (30)); completed &= teardown.Join (TimeSpan.FromSeconds (30)); Assert.True (completed, "Dispose/teardown race did not complete in time."); - if (disposeError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (disposeError); - if (teardownError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (teardownError); + if (disposeError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (disposeError).Throw (); + if (teardownError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (teardownError).Throw (); // The ignored public Dispose never closes the stream; the codec owner-teardown is the // real disposer. Proven by the closing thread being the teardown task, not the public From 62ab1c959c8c72633523211b1ccf694f861ee8d8 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Wed, 3 Jun 2026 16:24:40 +0200 Subject: [PATCH 23/29] Make singleton/lifecycle tests robust on the Mono interpreter (device CI) The iOS and Mac Catalyst legs run under the Mono interpreter, whose thread-pool starves under load, so tests driven through Task.Run / Parallel.For timed out there (desktop, .NET Framework and WASM were clean). Switch those tests to dedicated OS threads and skip the few techniques the device runtimes genuinely lack. - Stale-wrapper tests: drive the parked-disposer/creator races through a new RunOnThread/ThreadResult helper (dedicated threads) instead of Task.Run, keeping the IsBrowser-only skip and gaining device coverage. - SKManagedStreamTest.ConcurrentDisposeOfManyManagedStreamsIsSafe: use a smaller dedicated-thread count on mobile via RunConcurrent. - SKSingletonInitTest cold-start: skip on Android/iOS/Mac Catalyst too, where AssemblyDependencyResolver-based ALC isolation is unavailable. - AssertDeregistered: look up the base SKObject type to avoid the wrong-type THROW_OBJECT_EXCEPTIONS diagnostic on legal handle reuse. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SKHandleDictionaryStaleWrapperTest.cs | 19 ++-- .../SKHandleDictionaryTestHelpers.cs | 86 ++++++++++++++++++- tests/Tests/SkiaSharp/SKManagedStreamTest.cs | 13 ++- tests/Tests/SkiaSharp/SKSingletonInitTest.cs | 2 +- 4 files changed, 105 insertions(+), 15 deletions(-) diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs index d2df9b95e29..b4badc949f7 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs @@ -1,6 +1,5 @@ using System; using System.Threading; -using System.Threading.Tasks; using Xunit; using static SkiaSharp.Tests.SKHandleDictionaryTestHelpers; @@ -81,7 +80,7 @@ public void StaleWrapperCleanupDoesNotEvictGetOrAddReplacementForReusedHandle () try { // Public-dispose W1 on another thread: it claims isDisposed under the write lock, // releases the lock, then parks in DisposeManaged BEFORE Handle=0 -> DeregisterHandle runs. - var disposer = Task.Run (() => first.Dispose ()); + var disposer = RunOnThread (() => first.Dispose ()); try { Assert.True (enteredCleanup.Wait (10_000), "W1 never entered the managed-cleanup window."); Assert.True (first.IsDisposed); @@ -89,7 +88,7 @@ public void StaleWrapperCleanupDoesNotEvictGetOrAddReplacementForReusedHandle () // While W1 is parked mid-cleanup, a new native object reuses the handle. Run the // construction under a timeout: if cleanup ever regressed to holding the lock, // this would block on the lock and we want a deterministic failure, not a suite hang. - var creator = Task.Run (() => HandleDictionary.GetOrAddObject ( + var creator = RunOnThread (() => HandleDictionary.GetOrAddObject ( handle, owns: false, unrefExisting: false, (h, o) => new GatedCleanupObject (h))); Assert.True (creator.Wait (10_000), "Replacement construction blocked — cleanup may hold the lock."); second = creator.Result; @@ -141,7 +140,7 @@ public void StaleWrapperCleanupDoesNotEvictDirectCtorReplacementForReusedHandle GatedCleanupObject second = null; try { - var disposer = Task.Run (() => first.Dispose ()); + var disposer = RunOnThread (() => first.Dispose ()); try { Assert.True (enteredCleanup.Wait (10_000), "W1 never entered the managed-cleanup window."); Assert.True (first.IsDisposed); @@ -149,7 +148,7 @@ public void StaleWrapperCleanupDoesNotEvictDirectCtorReplacementForReusedHandle // Direct-construct the replacement: its base ctor RegisterHandle sees the disposed W1 // entry, skips the dispose-the-old branch (!obj.IsDisposed is false), and overwrites it. // Timeout-guarded so a cleanup-holds-the-lock regression fails deterministically. - var creator = Task.Run (() => new GatedCleanupObject (handle)); + var creator = RunOnThread (() => new GatedCleanupObject (handle)); Assert.True (creator.Wait (10_000), "Replacement construction blocked — cleanup may hold the lock."); second = creator.Result; @@ -194,29 +193,29 @@ public void TwoStaleWrappersDeregisteringDoNotEvictThirdReplacementForReusedHand var w1 = new GatedCleanupObject (handle, entered1, release1); GatedCleanupObject w2 = null, w3 = null; - Task disposer1 = null, disposer2 = null; + ThreadResult disposer1 = null, disposer2 = null; try { // Park W1 (disposed) in its cleanup window. - disposer1 = Task.Run (() => w1.Dispose ()); + disposer1 = RunOnThread (() => w1.Dispose ()); Assert.True (entered1.Wait (10_000), "W1 never entered the managed-cleanup window."); Assert.True (w1.IsDisposed); // Overwrite the disposed W1 with W2 (direct ctor -> RegisterHandle overwrite-stale branch), // then park W2 (disposed) in ITS cleanup window too. Now two stale wrappers are pending. - var make2 = Task.Run (() => new GatedCleanupObject (handle, entered2, release2)); + var make2 = RunOnThread (() => new GatedCleanupObject (handle, entered2, release2)); Assert.True (make2.Wait (10_000), "W2 construction blocked — cleanup may hold the lock."); w2 = make2.Result; Assert.NotSame (w1, w2); Assert.True (HandleDictionary.GetInstance (handle, out var afterW2)); Assert.Same (w2, afterW2); - disposer2 = Task.Run (() => w2.Dispose ()); + disposer2 = RunOnThread (() => w2.Dispose ()); Assert.True (entered2.Wait (10_000), "W2 never entered the managed-cleanup window."); Assert.True (w2.IsDisposed); // Overwrite the disposed W2 with the live W3. - var make3 = Task.Run (() => new GatedCleanupObject (handle)); + var make3 = RunOnThread (() => new GatedCleanupObject (handle)); Assert.True (make3.Wait (10_000), "W3 construction blocked — cleanup may hold the lock."); w3 = make3.Result; Assert.NotSame (w1, w3); diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs index c396ef498e3..e748ba4c4d7 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs @@ -172,13 +172,97 @@ public static void RunWithTimeout ( // lifecycle invariant we care about is narrower and address-reuse-safe: OUR specific disposed // wrapper must no longer be reachable through the registry under its handle. A different live // wrapper that legitimately reused the freed address is fine and must not fail the test. + // + // The lookup is intentionally typed as the base SKObject, NOT TSkiaObject. Under + // THROW_OBJECT_EXCEPTIONS (Debug), HandleDictionary.GetInstance throws when a reused handle + // holds a LIVE owning object of a DIFFERENT concrete type than T — exactly the legal + // address-reuse case above. Looking up as SKObject (the common base) can never hit that + // wrong-type diagnostic, so a parallel test's unrelated wrapper at the reused address yields a + // benign reference-mismatch instead of an InvalidOperationException. The reference-identity + // assertion below is unchanged: a disposed instance is filtered out by GetInstance (returns + // false), and any returned object is compared by reference against ours. public static void AssertDeregistered (IntPtr handle, TSkiaObject instance) where TSkiaObject : SKObject { - if (HandleDictionary.GetInstance (handle, out var found)) + if (HandleDictionary.GetInstance (handle, out var found)) Assert.False ( ReferenceEquals (found, instance), $"Disposed {typeof (TSkiaObject).Name} is still registered under handle 0x{handle.ToString ("x")}."); } + + // A dedicated-thread replacement for Task.Run used by the white-box stale-wrapper tests. Those + // tests need one worker that PARKS inside a gated managed-cleanup window while a SECOND worker + // concurrently registers a replacement for the same handle. On the mobile interpreter runtimes + // (iOS / Mac Catalyst / Android) the thread pool is scheduled so sparsely that a parked Task.Run + // worker plus a second Task.Run worker can exhaust the available pool threads, starving the + // handshake and hanging the test. A dedicated background Thread is always scheduled, so the + // interleaving the test relies on actually happens. Browser WASM has no OS threads at all and is + // skipped by the callers. Body exceptions are captured and resurfaced when the caller Waits. + public static ThreadResult RunOnThread (Action body) => + new ThreadResult (body); + + public static ThreadResult RunOnThread (Func body) => + new ThreadResult (body); + } + + // Fire-and-join handle around a single dedicated background thread, mirroring the slice of the Task + // API the stale-wrapper tests use: Wait(timeoutMs) joins with a deadline (false == still running == + // deadlock) and rethrows a captured body exception on the caller once the worker has joined. + internal sealed class ThreadResult + { + private readonly Thread thread; + private Exception error; + + internal ThreadResult (Action body) + { + thread = new Thread (() => { + try { + body (); + } catch (Exception ex) { + error = ex; + } + }) { IsBackground = true }; + thread.Start (); + } + + public bool Wait (int timeoutMs) + { + if (!thread.Join (timeoutMs)) + return false; + if (error != null) + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (error).Throw (); + return true; + } + } + + internal sealed class ThreadResult + { + private readonly Thread thread; + private Exception error; + private T result; + + internal ThreadResult (Func body) + { + thread = new Thread (() => { + try { + result = body (); + } catch (Exception ex) { + error = ex; + } + }) { IsBackground = true }; + thread.Start (); + } + + public bool Wait (int timeoutMs) + { + if (!thread.Join (timeoutMs)) + return false; + if (error != null) + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (error).Throw (); + return true; + } + + // Valid only after a successful Wait: the worker has joined, so the field write is published. + public T Result => result; } } diff --git a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs index c8b420c982b..5dadfb659ed 100644 --- a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs +++ b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs @@ -883,13 +883,20 @@ public void ConcurrentDisposeOfManyManagedStreamsIsSafe() { SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); - const int count = 128; + // Mobile interpreter runtimes (iOS / Mac Catalyst / Android) schedule the thread pool too + // sparsely to push 128 work items through the native dispose path before the timeout, so the + // stress is sized down there. The work is driven through dedicated threads (RunConcurrent), + // not Parallel.For: all N threads rendezvous at a barrier and then Dispose() simultaneously, + // which is both deterministic (no pool dependency) and a stronger test of the registry's + // concurrent-deregister path than pool-scheduled work items. + var count = (IsAndroid || IsIOS || IsMacCatalyst) ? 16 : 128; var streams = new List(count); for (var i = 0; i < count; i++) streams.Add(new SKManagedStream(CreateTestStream(), true)); - SKHandleDictionaryTestHelpers.RunWithTimeout( - () => Parallel.For(0, count, i => streams[i].Dispose()), + SKHandleDictionaryTestHelpers.RunConcurrent( + count, + i => streams[i].Dispose(), deadlockMessage: "Concurrent Dispose() of many managed streams deadlocked."); foreach (var stream in streams) diff --git a/tests/Tests/SkiaSharp/SKSingletonInitTest.cs b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs index 5cfc03d259b..70b0e4efeb3 100644 --- a/tests/Tests/SkiaSharp/SKSingletonInitTest.cs +++ b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs @@ -147,7 +147,7 @@ public void SKPaintConstructionDoesNotThrow() [SkippableFact] public void Issue3817_SKFontManagerDefaultDoesNotThrowFromColdStart() { - SkipOnPlatform(IsBrowser, "Browser WASM does not support custom AssemblyLoadContext / AssemblyDependencyResolver"); + SkipOnPlatform(IsBrowser || IsAndroid || IsIOS || IsMacCatalyst, "AssemblyDependencyResolver is not supported on this platform (browser WASM, Android, iOS, Mac Catalyst); cold-start ALC isolation is a desktop-only technique."); var alc = new IsolatedSkiaSharpLoadContext(typeof(SKFontManager).Assembly); try From 0804354582174738b3254a42e33ac5396afe09f1 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Wed, 3 Jun 2026 23:40:38 +0200 Subject: [PATCH 24/29] Isolate threading and refcount-sensitive tests into a serialized collection Slow CI agents (Windows netfx, small Azure VMs) could starve the thread- heavy concurrency tests when run in xUnit's parallel phase, and the exact native-refcount singleton test could be perturbed by other tests touching the same process-wide srgb-linear singleton concurrently. - Extract the SKManagedStream concurrency tests into a dedicated SKManagedStreamConcurrencyTest in the serialized collection, and route their raw threads through the RunOnThread helper. - Move SameColorSpaceCreatedDifferentWaysAreTheSameObject (exact refcount on the srgb-linear singleton) into SKSingletonInitTest and serialize that class; drain finalizers before capturing the baseline so a holder from the earlier parallel phase cannot decrement the count mid-test. - Tag the remaining HandleDictionary threading classes with the serialized collection so nothing runs concurrently with them. Test-only; no production code changes. Full suite: 5710 passed / 12 skipped / 0 failed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../HandleDictionaryThreadingCollection.cs | 32 + tests/Tests/SkiaSharp/SKColorSpaceTest.cs | 36 -- .../SKHandleDictionaryDisposeProtectedTest.cs | 1 + .../SKHandleDictionaryReentrancyTest.cs | 1 + .../SKHandleDictionaryReservationTest.cs | 1 + .../SKHandleDictionaryStaleWrapperTest.cs | 1 + .../SKHandleDictionaryTestHelpers.cs | 4 +- .../SKManagedStreamConcurrencyTest.cs | 561 +++++++++++++++++ tests/Tests/SkiaSharp/SKManagedStreamTest.cs | 562 ------------------ .../SkiaSharp/SKSingletonConcurrencyTest.cs | 1 + tests/Tests/SkiaSharp/SKSingletonInitTest.cs | 47 ++ 11 files changed, 647 insertions(+), 600 deletions(-) create mode 100644 tests/Tests/SkiaSharp/HandleDictionaryThreadingCollection.cs create mode 100644 tests/Tests/SkiaSharp/SKManagedStreamConcurrencyTest.cs diff --git a/tests/Tests/SkiaSharp/HandleDictionaryThreadingCollection.cs b/tests/Tests/SkiaSharp/HandleDictionaryThreadingCollection.cs new file mode 100644 index 00000000000..4d19ad004b9 --- /dev/null +++ b/tests/Tests/SkiaSharp/HandleDictionaryThreadingCollection.cs @@ -0,0 +1,32 @@ +using Xunit; + +namespace SkiaSharp.Tests +{ + // The dedicated white-box concurrency/lifecycle tests for HandleDictionary/SKObject drive their own + // deterministic interleavings (via gates and barriers on dedicated OS threads) and then join those + // threads against tight deadlines. A failed join is the signal for a genuine product deadlock. + // + // Under xUnit's default parallelism EVERY test class is its own collection and runs concurrently, and + // EVERY SKObject create/dispose/lookup across the whole suite contends on the single global + // HandleDictionary lock. On Windows that lock is a Win32 CRITICAL_SECTION (the #1383 STA-pump fix) with + // no reader concurrency, so the entire parallel suite serializes through one mutex. On a small/slow CI + // agent this can starve a parked dispose thread of CPU/lock ownership for >10s and trip these tight + // deadlines — a false "deadlock" that has nothing to do with the code under test. + // + // Placing all of these classes in one DisableParallelization collection makes them run sequentially in + // xUnit's non-parallel phase, AFTER the parallel phase has drained, so nothing else is hammering the + // global lock while their deadlines are ticking. This does NOT weaken coverage: each test produces its + // own interleaving, and the rest of the suite still exercises the global lock in parallel during the + // parallel phase. + // + // SKManagedStreamConcurrencyTest joins this collection for the same reason: its gated "public Dispose + // while a native lazy read is in-flight" tests park a real OS thread inside a managed Stream.Read() and + // then race a public Dispose against codec teardown under a 30s join deadline. Running it in the + // drained non-parallel phase removes both the global-lock contention above AND any thread-pool + // starvation, so the parked reader and the racing disposer always get CPU before the deadline. + [CollectionDefinition (HandleDictionaryThreadingCollection.Name, DisableParallelization = true)] + public sealed class HandleDictionaryThreadingCollection + { + public const string Name = "HandleDictionary threading (serialized)"; + } +} diff --git a/tests/Tests/SkiaSharp/SKColorSpaceTest.cs b/tests/Tests/SkiaSharp/SKColorSpaceTest.cs index bd53968b607..5fa826c6ed9 100644 --- a/tests/Tests/SkiaSharp/SKColorSpaceTest.cs +++ b/tests/Tests/SkiaSharp/SKColorSpaceTest.cs @@ -391,42 +391,6 @@ public void CicpPqPngImageHasPqTransferFunction() Assert.False(info.ColorSpace.IsSrgb); } - [SkippableFact] - public void SameColorSpaceCreatedDifferentWaysAreTheSameObject() - { - // get the first instance of the sRGB Linear and capture the steady-state refcount - var colorspace1 = SKColorSpace.CreateSrgbLinear(); - Assert.True(colorspace1.IgnorePublicDispose); - var baselineRefCount = colorspace1.GetReferenceCount(); - - // create a new one with the same parameters, which will return the same instance - var colorspace2 = SKColorSpace.CreateRgb(SKColorSpaceTransferFn.Linear, SKColorSpaceXyz.Srgb); - Assert.True(colorspace2.IgnorePublicDispose); - Assert.Same(colorspace1, colorspace2); - Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); - Assert.Equal(baselineRefCount, colorspace2.GetReferenceCount()); - - // create a different one manually, which will return a new instance - var colorspace3 = SKColorSpace.CreateRgb( - new SKColorSpaceTransferFn { A = 0.6f, B = 0.5f, C = 0.4f, D = 0.3f, E = 0.2f, F = 0.1f }, - SKColorSpaceXyz.Identity); - Assert.NotSame(colorspace1, colorspace3); - Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); - Assert.Equal(baselineRefCount, colorspace2.GetReferenceCount()); - - colorspace3.Dispose(); - Assert.True(colorspace3.IsDisposed); - Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); - - colorspace2.Dispose(); - Assert.False(colorspace2.IsDisposed); - Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); - - colorspace1.Dispose(); - Assert.False(colorspace1.IsDisposed); - Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); - } - private static void AssertMatrix(float[] expected, SKMatrix44 actual) { var actualArray = actual diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs index c9ff1fc4d18..5e94373e271 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs @@ -15,6 +15,7 @@ namespace SkiaSharp.Tests // gets back a live, protected wrapper, even when racing a concurrent public Dispose() for the same // handle. They use a fake, non-owning SKObject (see SKHandleDictionaryTestHelpers) so no native // memory is touched. + [Collection (HandleDictionaryThreadingCollection.Name)] public class SKHandleDictionaryDisposeProtectedTest : SKTest { // --- dispose-protected fresh construction sets the IgnorePublicDispose latch --- diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs index 99f8062d929..85407d50128 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs @@ -11,6 +11,7 @@ namespace SkiaSharp.Tests // Verified empirically (macOS) before being written: // reentrant GetInstance (read) -> succeeds // nested GetOrAddObject (create) -> throws LockRecursionException (non-Windows) + [Collection (HandleDictionaryThreadingCollection.Name)] public class SKHandleDictionaryReentrancyTest : SKTest { // A factory MAY read the dictionary. GetInstance takes a read lock, and a thread already diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs index 3fe17c089e7..eb4cbc821f8 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs @@ -22,6 +22,7 @@ namespace SkiaSharp.Tests // LockRecursionPolicy.NoRecursion on non-Windows), a nested GetOrAddObject would throw // LockRecursionException rather than recurse — and on Windows (recursive CRITICAL_SECTION) it // would not — so any such test is platform-dependent and is omitted by design. + [Collection (HandleDictionaryThreadingCollection.Name)] public class SKHandleDictionaryReservationTest : SKTest { // --- Exactly-once construction under contention --- diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs index b4badc949f7..05b2694c3ee 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs @@ -14,6 +14,7 @@ namespace SkiaSharp.Tests // exact window (via GatedCleanupObject) and prove the late DeregisterHandle of the stale wrapper // never evicts the live replacement and never raises a THROW_OBJECT_EXCEPTIONS diagnostic. They use // a fake, non-owning SKObject (see SKHandleDictionaryTestHelpers) so no native memory is touched. + [Collection (HandleDictionaryThreadingCollection.Name)] public class SKHandleDictionaryStaleWrapperTest : SKTest { // --- Address reuse (ABA): a handle freed then re-used for a new native object. The registry diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs index e748ba4c4d7..10ac97feee5 100644 --- a/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs @@ -190,8 +190,8 @@ public static void AssertDeregistered (IntPtr handle, TSkiaObject i $"Disposed {typeof (TSkiaObject).Name} is still registered under handle 0x{handle.ToString ("x")}."); } - // A dedicated-thread replacement for Task.Run used by the white-box stale-wrapper tests. Those - // tests need one worker that PARKS inside a gated managed-cleanup window while a SECOND worker + // A dedicated-thread replacement for Task.Run used by the white-box stale-wrapper tests and the + // managed-stream concurrency tests. Those tests need one worker that PARKS inside a gated window while a SECOND worker // concurrently registers a replacement for the same handle. On the mobile interpreter runtimes // (iOS / Mac Catalyst / Android) the thread pool is scheduled so sparsely that a parked Task.Run // worker plus a second Task.Run worker can exhaust the available pool threads, starving the diff --git a/tests/Tests/SkiaSharp/SKManagedStreamConcurrencyTest.cs b/tests/Tests/SkiaSharp/SKManagedStreamConcurrencyTest.cs new file mode 100644 index 00000000000..11d654548fb --- /dev/null +++ b/tests/Tests/SkiaSharp/SKManagedStreamConcurrencyTest.cs @@ -0,0 +1,561 @@ +using System; +using System.IO; +using System.Linq; +using Xunit; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace SkiaSharp.Tests +{ + [Collection (HandleDictionaryThreadingCollection.Name)] + public class SKManagedStreamConcurrencyTest : SKTest + { + [SkippableFact] + public void ConcurrentDisposeOfSameManagedStreamIsIdempotent() + { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + + // Many threads racing Dispose() on the SAME wrapper must funnel through the single + // isDisposed CAS: one cleanup, native freed once, destroy callback flips fromNative once. + var stream = new SKManagedStream(CreateTestStream(), true); + var handle = stream.Handle; + + SKHandleDictionaryTestHelpers.RunWithTimeout( + () => Parallel.For(0, 64, _ => stream.Dispose()), + deadlockMessage: "Concurrent Dispose() of the same managed stream deadlocked."); + + Assert.Equal(1, stream.fromNative); + Assert.True(stream.IsDisposed); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + + [SkippableFact] + public void ConcurrentDisposeOfManyManagedStreamsIsSafe() + { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + + // Mobile interpreter runtimes (iOS / Mac Catalyst / Android) schedule the thread pool too + // sparsely to push 128 work items through the native dispose path before the timeout, so the + // stress is sized down there. The work is driven through dedicated threads (RunConcurrent), + // not Parallel.For: all N threads rendezvous at a barrier and then Dispose() simultaneously, + // which is both deterministic (no pool dependency) and a stronger test of the registry's + // concurrent-deregister path than pool-scheduled work items. + var count = (IsAndroid || IsIOS || IsMacCatalyst) ? 16 : 128; + var streams = new List(count); + for (var i = 0; i < count; i++) + streams.Add(new SKManagedStream(CreateTestStream(), true)); + + SKHandleDictionaryTestHelpers.RunConcurrent( + count, + i => streams[i].Dispose(), + deadlockMessage: "Concurrent Dispose() of many managed streams deadlocked."); + + foreach (var stream in streams) + { + Assert.Equal(1, stream.fromNative); + Assert.True(stream.IsDisposed); + } + } + + // After SKCodec.Create reparents the managed stream (OwnsHandle=false, IgnorePublicDispose + // latched under the lock), the user STILL holds the wrapper and may call Dispose() on it + // from another thread. The PR's lock-paired public Dispose must IGNORE that call so the codec + // keeps reading through a live stream and remains its sole disposer. Before the LAZY-reparent + // fix an eager public close here tore the native stream out from under an in-flight native read + // -> use-after-free / host crash. The existing concurrency tests only race Dispose() against + // itself on an OWNED stream; these two pin the reparented supported concurrency: (1) an ignored + // public Dispose racing a lazy native read, and (2) an ignored public Dispose racing the codec's + // owner-teardown (which must still be the single, exactly-once disposer of the wrapper). + + [SkippableFact] + public unsafe void PublicDisposeWhileCodecReadIsInFlightIsIgnored() + { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + + // DETERMINISTIC version of the race: park the native lazy read INSIDE the .NET + // Stream.Read() callback, fire the (reparented) wrapper's public Dispose() while the + // native read is provably in-flight, then release the read. The public Dispose must be + // ignored (IgnorePublicDispose latched), so the in-flight native read completes against a + // live stream and GetPixels succeeds. This is the exact use-after-free the LAZY-reparent + // fix prevents — an eager public close here would close the .NET stream mid-read. + var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); + + using var readEntered = new ManualResetEventSlim(false); + using var releaseRead = new ManualResetEventSlim(false); + + var dotnet = new GatedCountingStream(bytes); + var stream = new SKManagedStream(dotnet, true); + var handle = stream.Handle; + var codec = SKCodec.Create(stream); + ThreadResult reader = null; + try + { + Assert.False(stream.OwnsHandle); + Assert.True(stream.IgnorePublicDispose); + + // Arm only AFTER Create() so the gate trips on a GetPixels read, not header parsing. + dotnet.Arm(readEntered, releaseRead); + + reader = SKHandleDictionaryTestHelpers.RunOnThread (() => codec.GetPixels (out _)); + + Assert.True(readEntered.Wait(TimeSpan.FromSeconds(30)), "Native lazy read never entered the managed stream."); + + // The native read is parked inside Read(); a public Dispose now must be a no-op. + stream.Dispose(); + Assert.False(stream.IsDisposed); + + releaseRead.Set(); + Assert.True(reader.Wait(30_000), "GetPixels did not complete after the read was released."); + + // If the ignored Dispose had wrongly closed the .NET stream, the parked inner.Read would + // have thrown and GetPixels would not be Success; DisposeCount would also not be 0. + Assert.Equal(SKCodecResult.Success, reader.Result); + Assert.False(stream.IsDisposed); + Assert.Equal(0, dotnet.DisposeCount); + } + finally + { + // Drain the reader before tearing the codec down so no background decode runs against a + // disposed codec. Swallow here so a primary in-try assertion failure is not masked. + releaseRead.Set(); + try { reader?.Wait(30_000); } catch { } + codec.Dispose(); + } + + // The codec is the sole disposer; the underlying .NET stream was closed exactly once. + Assert.True(stream.IsDisposed); + Assert.Equal(1, dotnet.DisposeCount); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + + [SkippableFact] + public unsafe void PublicDisposeAroundLazyCodecReadIsIgnoredStress() + { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + + // STRESS version: barrier-synchronised public Dispose vs a full GetPixels, repeated many + // times to shake out interleavings beyond the single overlap the deterministic gate pins. + // (The barrier only co-starts the two operations; it does NOT guarantee Dispose overlaps an + // in-flight Read — that exact overlap is proven deterministically by the gated test above.) + const int iterations = 100; + var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); + + for (var i = 0; i < iterations; i++) + { + var stream = new SKManagedStream(new MemoryStream(bytes), true); + var handle = stream.Handle; + var codec = SKCodec.Create(stream); + try + { + Assert.False(stream.OwnsHandle); + Assert.True(stream.IgnorePublicDispose); + + var result = SKCodecResult.InternalError; + using (var barrier = new Barrier(2)) + { + // Dedicated threads (not Task.Run) so the Barrier(2) always has both participants + // present even under full-suite load; see RunConcurrent rationale. + var read = SKHandleDictionaryTestHelpers.RunOnThread (() => { barrier.SignalAndWait (); return codec.GetPixels (out _); }); + var dispose = SKHandleDictionaryTestHelpers.RunOnThread (() => { barrier.SignalAndWait (); stream.Dispose (); }); + Assert.True (read.Wait (30_000) & dispose.Wait (30_000), "Dispose/GetPixels race did not complete in time."); + result = read.Result; + } + + // The lazy native read saw a live stream; the racing public Dispose was a no-op. + Assert.Equal(SKCodecResult.Success, result); + Assert.False(stream.IsDisposed); + Assert.True(HandleDictionary.GetInstance(handle, out _)); + } + finally + { + codec.Dispose(); + } + + // The codec is the sole disposer; teardown ran after the race. + Assert.Equal(1, stream.fromNative); + Assert.True(stream.IsDisposed); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + } + + [SkippableFact] + public unsafe void PublicDisposeRacingCodecTeardownClosesManagedStreamExactlyOnce() + { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + + // Ignored public Dispose (no-ops on IgnorePublicDispose) racing the codec's owner-teardown + // (the sole disposer, via DisposeUnownedManaged -> child.DisposeInternal). The underlying + // .NET stream must be closed EXACTLY once — proven by DisposeCount. fromNative is only a + // one-way latch (Interlocked.Exchange), so fromNative==1 asserts the native destroy callback + // was OBSERVED, not that it fired exactly once; DisposeCount carries the exactly-once proof. + const int iterations = 200; + var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); + + for (var i = 0; i < iterations; i++) + { + var dotnet = new GatedCountingStream(bytes); + var stream = new SKManagedStream(dotnet, true); + var handle = stream.Handle; + var codec = SKCodec.Create(stream); + try + { + Assert.False(stream.OwnsHandle); + Assert.True(stream.IgnorePublicDispose); + + using (var barrier = new Barrier(2)) + { + var teardownThreadId = 0; + // Dedicated threads (not Task.Run) so the Barrier(2) always has both participants. + var dispose = SKHandleDictionaryTestHelpers.RunOnThread (() => { barrier.SignalAndWait (); stream.Dispose (); }); + var teardown = SKHandleDictionaryTestHelpers.RunOnThread (() => { barrier.SignalAndWait (); teardownThreadId = Environment.CurrentManagedThreadId; codec.Dispose (); }); + Assert.True (dispose.Wait (30_000) & teardown.Wait (30_000), "Dispose/teardown race did not complete in time."); + + // The ignored public Dispose never closes the stream; the codec owner-teardown is the + // real disposer. Proven by the closing thread being the teardown task, not the public + // Dispose task — DisposeCount==1 alone could not distinguish which path won. + Assert.Equal (teardownThreadId, dotnet.FirstDisposeThreadId); + + // Keep both wrappers rooted across the race assertion so a GC + finalizer + // can never flip FirstDisposeThreadId from the finalizer thread mid-assert. + GC.KeepAlive (stream); + GC.KeepAlive (dotnet); + } + } + finally + { + // Idempotent: redundant if the racing thread already tore the codec down. + codec.Dispose(); + } + + // Ignored public Dispose + single owner teardown => one .NET stream close, one latch flip. + Assert.Equal(1, dotnet.DisposeCount); + Assert.Equal(1, stream.fromNative); + Assert.True(stream.IsDisposed); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + } + + [SkippableFact] + public unsafe void NonSeekableStreamReparentedToCodecTearsDownNestedStreamExactlyOnce() + { + // A NON-SEEKABLE stream routes SKCodec.Create through SKFrontBufferedManagedStream, which + // holds a private *inner* SKManagedStream wrapping the user's .NET stream. Only the OUTER + // front-buffered wrapper is reparented onto the codec (OwnsHandle=false, IgnorePublicDispose + // =true); the inner SKManagedStream is UNTOUCHED (still owns its handle, still forwards public + // Dispose). The user's racing fb.Dispose() must be a no-op, and codec teardown must walk + // fb -> inner -> .NET stream and close the .NET stream EXACTLY once. We supply the inner + // explicitly (public ctor) so both wrappers' deregistration is asserted without reflection. + var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); + + var dotnet = new NonSeekableGatedCountingStream(bytes); + var inner = new SKManagedStream(dotnet, true); + var innerHandle = inner.Handle; + var fb = new SKFrontBufferedManagedStream(inner, SKCodec.MinBufferedBytesNeeded, true); + var fbHandle = fb.Handle; + var codec = SKCodec.Create(fb); + try + { + Assert.NotNull(codec); + + // Only the outer front-buffered wrapper is reparented; the inner stays a normal owner. + Assert.False(fb.OwnsHandle); + Assert.True(fb.IgnorePublicDispose); + Assert.True(inner.OwnsHandle); + Assert.False(inner.IgnorePublicDispose); + + Assert.Equal(SKCodecResult.Success, codec.GetPixels(out _)); + + // The user still holds fb and (wrongly) disposes it: it is reparented, so this is a no-op. + fb.Dispose(); + Assert.False(fb.IsDisposed); + Assert.False(inner.IsDisposed); + Assert.Equal(0, dotnet.DisposeCount); + } + finally + { + codec.Dispose(); + } + + // Codec teardown is the sole disposer: fb -> inner -> .NET stream, closed exactly once. + Assert.True(fb.IsDisposed); + Assert.True(inner.IsDisposed); + Assert.Equal(1, dotnet.DisposeCount); + SKHandleDictionaryTestHelpers.AssertDeregistered(fbHandle, fb); + SKHandleDictionaryTestHelpers.AssertDeregistered(innerHandle, inner); + } + + [SkippableFact] + public unsafe void PublicDisposeWhileFrontBufferedCodecReadIsInFlightIsIgnored() + { + SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + + // DETERMINISTIC in-flight-read race for the NESTED non-seekable graph. Park the native lazy + // read inside the underlying .NET Stream.Read() (reached through fb's front buffer -> inner + // SKManagedStream), fire the reparented fb's public Dispose() while that read is provably + // in-flight, then release. The ignored public Dispose must not close anything mid-read, so + // GetPixels succeeds and the .NET stream is closed exactly once by codec teardown. + var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); + + using var readEntered = new ManualResetEventSlim(false); + using var releaseRead = new ManualResetEventSlim(false); + + var dotnet = new NonSeekableGatedCountingStream(bytes); + var fb = new SKFrontBufferedManagedStream(dotnet, SKCodec.MinBufferedBytesNeeded, true); + var fbHandle = fb.Handle; + var codec = SKCodec.Create(fb); + ThreadResult reader = null; + try + { + Assert.False(fb.OwnsHandle); + Assert.True(fb.IgnorePublicDispose); + + // Arm only AFTER Create() so the gate trips on a GetPixels read past the front buffer, not + // on the header bytes buffered during format detection. + dotnet.Arm(readEntered, releaseRead); + + reader = SKHandleDictionaryTestHelpers.RunOnThread (() => codec.GetPixels (out _)); + + Assert.True(readEntered.Wait(TimeSpan.FromSeconds(30)), "Native lazy read never reached the underlying non-seekable stream."); + + // The native read is parked inside the underlying Read(); a public Dispose now must no-op. + fb.Dispose(); + Assert.False(fb.IsDisposed); + Assert.Equal(0, dotnet.DisposeCount); + + releaseRead.Set(); + Assert.True(reader.Wait(30_000), "GetPixels did not complete after the read was released."); + + Assert.Equal(SKCodecResult.Success, reader.Result); + Assert.False(fb.IsDisposed); + Assert.Equal(0, dotnet.DisposeCount); + } + finally + { + releaseRead.Set(); + try { reader?.Wait(30_000); } catch { } + codec.Dispose(); + } + + // The codec is the sole disposer; the underlying .NET stream was closed exactly once. + Assert.True(fb.IsDisposed); + Assert.Equal(1, dotnet.DisposeCount); + SKHandleDictionaryTestHelpers.AssertDeregistered(fbHandle, fb); + } + + [SkippableFact] + public unsafe void InvalidNonSeekableStreamFailedCodecCreateClosesNestedStreamExactlyOnce() + { + // FAILURE path for the nested non-seekable graph: when the codec cannot be created, Create + // still transfers ownership to a null native object, which disposes the front-buffered + // wrapper immediately. That teardown must walk fb -> inner -> .NET stream and close it + // exactly once, and both wrappers must deregister — no leak, no double-free. + var bytes = new byte[256]; + for (var i = 0; i < bytes.Length; i++) + bytes[i] = (byte)(i + 1); + + var dotnet = new NonSeekableGatedCountingStream(bytes); + var inner = new SKManagedStream(dotnet, true); + var innerHandle = inner.Handle; + var fb = new SKFrontBufferedManagedStream(inner, SKCodec.MinBufferedBytesNeeded, true); + var fbHandle = fb.Handle; + + var codec = SKCodec.Create(fb, out var result); + + Assert.Null(codec); + Assert.NotEqual(SKCodecResult.Success, result); + + // The failed Create disposed fb immediately, which tore down the whole nested chain once. + Assert.True(fb.IsDisposed); + Assert.True(inner.IsDisposed); + Assert.Equal(1, dotnet.DisposeCount); + SKHandleDictionaryTestHelpers.AssertDeregistered(fbHandle, fb); + SKHandleDictionaryTestHelpers.AssertDeregistered(innerHandle, inner); + } + + [SkippableFact] + public unsafe void InvalidSeekableManagedStreamFailedCodecCreateClosesStreamExactlyOnce() + { + // FAILURE path for the FLAT seekable graph (no front-buffering): a seekable managed Stream + // of invalid bytes is wrapped in a single SKManagedStream. When the codec cannot be created, + // Create transfers ownership to a null native object, so RevokeOwnership(null) disposes the + // wrapper immediately. That teardown must close the .NET stream EXACTLY once and deregister + // the wrapper — no leak, no double-free. Complements the non-seekable/front-buffered failure + // test above, which exercises a different (nested) teardown graph. + var bytes = new byte[256]; + for (var i = 0; i < bytes.Length; i++) + bytes[i] = (byte)(i + 1); + + var dotnet = new GatedCountingStream(bytes); + var stream = new SKManagedStream(dotnet, true); + var handle = stream.Handle; + + Assert.True(stream.OwnsHandle); + Assert.True(SKObject.GetInstance(handle, out _)); + + var codec = SKCodec.Create(stream, out var result); + + Assert.Null(codec); + Assert.NotEqual(SKCodecResult.Success, result); + + // Failed Create disposed the flat wrapper, closing the .NET stream exactly once. + Assert.True(stream.IsDisposed); + Assert.Equal(1, dotnet.DisposeCount); + SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); + } + + [SkippableFact] + public unsafe void FrontBufferedCodecWithoutOwnershipDoesNotCloseUnderlyingStream() + { + // disposeUnderlyingStream:false — the user keeps ownership of the .NET stream. The nested + // SKManagedStream is still disposed by codec teardown, but it must NOT close the user's .NET + // stream. We then close it ourselves to prove the count is driven solely by the user, not the + // codec chain. + var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); + + var dotnet = new NonSeekableGatedCountingStream(bytes); + var fb = new SKFrontBufferedManagedStream(dotnet, SKCodec.MinBufferedBytesNeeded, false); + var fbHandle = fb.Handle; + var codec = SKCodec.Create(fb); + try + { + Assert.NotNull(codec); + Assert.False(fb.OwnsHandle); + Assert.True(fb.IgnorePublicDispose); + Assert.Equal(SKCodecResult.Success, codec.GetPixels(out _)); + } + finally + { + codec.Dispose(); + } + + // Codec teardown disposed the wrappers but, lacking ownership, left the .NET stream open. + Assert.True(fb.IsDisposed); + SKHandleDictionaryTestHelpers.AssertDeregistered(fbHandle, fb); + Assert.Equal(0, dotnet.DisposeCount); + + // The user is the only owner: closing now is the first and only close. + dotnet.Dispose(); + Assert.Equal(1, dotnet.DisposeCount); + } + + // A seekable .NET Stream over a byte buffer that (a) counts how many times it is disposed and + // (b) can park the FIRST read after Arm() until released — used to drive a deterministic + // "public Dispose while the native lazy read is in-flight" race and to count exact teardown. + private sealed class GatedCountingStream : Stream + { + private readonly MemoryStream inner; + private ManualResetEventSlim readEntered; + private ManualResetEventSlim releaseRead; + private int gateArmed; + private int disposeCount; + private int firstDisposeThreadId; + + public GatedCountingStream(byte[] bytes) => inner = new MemoryStream(bytes, writable: false); + + public int DisposeCount => Volatile.Read(ref disposeCount); + + // Managed thread id of whoever closed the .NET stream first. Lets a test prove the codec + // owner-teardown — not the ignored public Dispose — was the actual disposer. + public int FirstDisposeThreadId => Volatile.Read(ref firstDisposeThreadId); + + public void Arm(ManualResetEventSlim entered, ManualResetEventSlim release) + { + readEntered = entered; + releaseRead = release; + Volatile.Write(ref gateArmed, 1); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (Interlocked.CompareExchange(ref gateArmed, 0, 1) == 1) + { + readEntered.Set(); + releaseRead.Wait(); + } + return inner.Read(buffer, offset, count); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (Interlocked.Increment(ref disposeCount) == 1) + Volatile.Write(ref firstDisposeThreadId, Environment.CurrentManagedThreadId); + // Actually close the backing buffer: a premature close during an in-flight read + // then surfaces as an ObjectDisposedException from the parked inner.Read. + inner.Dispose(); + } + base.Dispose(disposing); + } + + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => false; + public override long Length => inner.Length; + public override long Position { get => inner.Position; set => inner.Position = value; } + public override long Seek(long offset, SeekOrigin origin) => inner.Seek(offset, origin); + public override void Flush() => inner.Flush(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } + + // A NON-SEEKABLE .NET Stream over a byte buffer that (a) counts how many times it is disposed, + // (b) records the disposing thread id, and (c) can park the first read after Arm() until + // released. Non-seekable so SKCodec.Create routes it through the SKFrontBufferedManagedStream + // (nested SKManagedStream) path — a different teardown graph from the flat seekable wrapper. + private sealed class NonSeekableGatedCountingStream : Stream + { + private readonly MemoryStream inner; + private ManualResetEventSlim readEntered; + private ManualResetEventSlim releaseRead; + private int gateArmed; + private int disposeCount; + private int firstDisposeThreadId; + + public NonSeekableGatedCountingStream(byte[] bytes) => inner = new MemoryStream(bytes, writable: false); + + public int DisposeCount => Volatile.Read(ref disposeCount); + + public int FirstDisposeThreadId => Volatile.Read(ref firstDisposeThreadId); + + public void Arm(ManualResetEventSlim entered, ManualResetEventSlim release) + { + readEntered = entered; + releaseRead = release; + Volatile.Write(ref gateArmed, 1); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (Interlocked.CompareExchange(ref gateArmed, 0, 1) == 1) + { + readEntered.Set(); + releaseRead.Wait(); + } + return inner.Read(buffer, offset, count); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (Interlocked.Increment(ref disposeCount) == 1) + Volatile.Write(ref firstDisposeThreadId, Environment.CurrentManagedThreadId); + inner.Dispose(); + } + base.Dispose(disposing); + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position { get => inner.Position; set => throw new NotSupportedException(); } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void Flush() => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } + } +} diff --git a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs index 5dadfb659ed..858450a7a3e 100644 --- a/tests/Tests/SkiaSharp/SKManagedStreamTest.cs +++ b/tests/Tests/SkiaSharp/SKManagedStreamTest.cs @@ -859,568 +859,6 @@ public void DisposingManagedStreamTwiceIsNoOp() SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); } - [SkippableFact] - public void ConcurrentDisposeOfSameManagedStreamIsIdempotent() - { - SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); - - // Many threads racing Dispose() on the SAME wrapper must funnel through the single - // isDisposed CAS: one cleanup, native freed once, destroy callback flips fromNative once. - var stream = new SKManagedStream(CreateTestStream(), true); - var handle = stream.Handle; - - SKHandleDictionaryTestHelpers.RunWithTimeout( - () => Parallel.For(0, 64, _ => stream.Dispose()), - deadlockMessage: "Concurrent Dispose() of the same managed stream deadlocked."); - - Assert.Equal(1, stream.fromNative); - Assert.True(stream.IsDisposed); - SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); - } - - [SkippableFact] - public void ConcurrentDisposeOfManyManagedStreamsIsSafe() - { - SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); - - // Mobile interpreter runtimes (iOS / Mac Catalyst / Android) schedule the thread pool too - // sparsely to push 128 work items through the native dispose path before the timeout, so the - // stress is sized down there. The work is driven through dedicated threads (RunConcurrent), - // not Parallel.For: all N threads rendezvous at a barrier and then Dispose() simultaneously, - // which is both deterministic (no pool dependency) and a stronger test of the registry's - // concurrent-deregister path than pool-scheduled work items. - var count = (IsAndroid || IsIOS || IsMacCatalyst) ? 16 : 128; - var streams = new List(count); - for (var i = 0; i < count; i++) - streams.Add(new SKManagedStream(CreateTestStream(), true)); - - SKHandleDictionaryTestHelpers.RunConcurrent( - count, - i => streams[i].Dispose(), - deadlockMessage: "Concurrent Dispose() of many managed streams deadlocked."); - - foreach (var stream in streams) - { - Assert.Equal(1, stream.fromNative); - Assert.True(stream.IsDisposed); - } - } - - // After SKCodec.Create reparents the managed stream (OwnsHandle=false, IgnorePublicDispose - // latched under the lock), the user STILL holds the wrapper and may call Dispose() on it - // from another thread. The PR's lock-paired public Dispose must IGNORE that call so the codec - // keeps reading through a live stream and remains its sole disposer. Before the LAZY-reparent - // fix an eager public close here tore the native stream out from under an in-flight native read - // -> use-after-free / host crash. The existing concurrency tests only race Dispose() against - // itself on an OWNED stream; these two pin the reparented supported concurrency: (1) an ignored - // public Dispose racing a lazy native read, and (2) an ignored public Dispose racing the codec's - // owner-teardown (which must still be the single, exactly-once disposer of the wrapper). - - [SkippableFact] - public unsafe void PublicDisposeWhileCodecReadIsInFlightIsIgnored() - { - SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); - - // DETERMINISTIC version of the race: park the native lazy read INSIDE the .NET - // Stream.Read() callback, fire the (reparented) wrapper's public Dispose() while the - // native read is provably in-flight, then release the read. The public Dispose must be - // ignored (IgnorePublicDispose latched), so the in-flight native read completes against a - // live stream and GetPixels succeeds. This is the exact use-after-free the LAZY-reparent - // fix prevents — an eager public close here would close the .NET stream mid-read. - var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); - - using var readEntered = new ManualResetEventSlim(false); - using var releaseRead = new ManualResetEventSlim(false); - - var dotnet = new GatedCountingStream(bytes); - var stream = new SKManagedStream(dotnet, true); - var handle = stream.Handle; - var codec = SKCodec.Create(stream); - Task reader = null; - try - { - Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); - - // Arm only AFTER Create() so the gate trips on a GetPixels read, not header parsing. - dotnet.Arm(readEntered, releaseRead); - - var result = SKCodecResult.InternalError; - reader = Task.Run(() => result = codec.GetPixels(out _)); - - Assert.True(readEntered.Wait(TimeSpan.FromSeconds(30)), "Native lazy read never entered the managed stream."); - - // The native read is parked inside Read(); a public Dispose now must be a no-op. - stream.Dispose(); - Assert.False(stream.IsDisposed); - - releaseRead.Set(); - Assert.True(reader.Wait(TimeSpan.FromSeconds(30)), "GetPixels did not complete after the read was released."); - - // If the ignored Dispose had wrongly closed the .NET stream, the parked inner.Read would - // have thrown and GetPixels would not be Success; DisposeCount would also not be 0. - Assert.Equal(SKCodecResult.Success, result); - Assert.False(stream.IsDisposed); - Assert.Equal(0, dotnet.DisposeCount); - } - finally - { - // Drain the reader before tearing the codec down so no background decode runs against a - // disposed codec. Swallow here so a primary in-try assertion failure is not masked. - releaseRead.Set(); - try { reader?.Wait(TimeSpan.FromSeconds(30)); } catch { } - codec.Dispose(); - } - - // The codec is the sole disposer; the underlying .NET stream was closed exactly once. - Assert.True(stream.IsDisposed); - Assert.Equal(1, dotnet.DisposeCount); - SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); - } - - [SkippableFact] - public unsafe void PublicDisposeAroundLazyCodecReadIsIgnoredStress() - { - SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); - - // STRESS version: barrier-synchronised public Dispose vs a full GetPixels, repeated many - // times to shake out interleavings beyond the single overlap the deterministic gate pins. - // (The barrier only co-starts the two operations; it does NOT guarantee Dispose overlaps an - // in-flight Read — that exact overlap is proven deterministically by the gated test above.) - const int iterations = 100; - var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); - - for (var i = 0; i < iterations; i++) - { - var stream = new SKManagedStream(new MemoryStream(bytes), true); - var handle = stream.Handle; - var codec = SKCodec.Create(stream); - try - { - Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); - - var result = SKCodecResult.InternalError; - using (var barrier = new Barrier(2)) - { - // Dedicated threads (not Task.Run) so the Barrier(2) always has both participants - // present even under full-suite load; see RunConcurrent rationale. - Exception readError = null, disposeError = null; - var read = new Thread (() => { try { barrier.SignalAndWait (); result = codec.GetPixels (out _); } catch (Exception ex) { readError = ex; } }) { IsBackground = true }; - var dispose = new Thread (() => { try { barrier.SignalAndWait (); stream.Dispose (); } catch (Exception ex) { disposeError = ex; } }) { IsBackground = true }; - read.Start (); - dispose.Start (); - var completed = read.Join (TimeSpan.FromSeconds (30)); - completed &= dispose.Join (TimeSpan.FromSeconds (30)); - Assert.True (completed, "Dispose/GetPixels race did not complete in time."); - if (readError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (readError).Throw (); - if (disposeError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (disposeError).Throw (); - } - - // The lazy native read saw a live stream; the racing public Dispose was a no-op. - Assert.Equal(SKCodecResult.Success, result); - Assert.False(stream.IsDisposed); - Assert.True(HandleDictionary.GetInstance(handle, out _)); - } - finally - { - codec.Dispose(); - } - - // The codec is the sole disposer; teardown ran after the race. - Assert.Equal(1, stream.fromNative); - Assert.True(stream.IsDisposed); - SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); - } - } - - [SkippableFact] - public unsafe void PublicDisposeRacingCodecTeardownClosesManagedStreamExactlyOnce() - { - SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); - - // Ignored public Dispose (no-ops on IgnorePublicDispose) racing the codec's owner-teardown - // (the sole disposer, via DisposeUnownedManaged -> child.DisposeInternal). The underlying - // .NET stream must be closed EXACTLY once — proven by DisposeCount. fromNative is only a - // one-way latch (Interlocked.Exchange), so fromNative==1 asserts the native destroy callback - // was OBSERVED, not that it fired exactly once; DisposeCount carries the exactly-once proof. - const int iterations = 200; - var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); - - for (var i = 0; i < iterations; i++) - { - var dotnet = new GatedCountingStream(bytes); - var stream = new SKManagedStream(dotnet, true); - var handle = stream.Handle; - var codec = SKCodec.Create(stream); - try - { - Assert.False(stream.OwnsHandle); - Assert.True(stream.IgnorePublicDispose); - - using (var barrier = new Barrier(2)) - { - var teardownThreadId = 0; - // Dedicated threads (not Task.Run) so the Barrier(2) always has both participants. - Exception disposeError = null, teardownError = null; - var dispose = new Thread (() => { try { barrier.SignalAndWait (); stream.Dispose (); } catch (Exception ex) { disposeError = ex; } }) { IsBackground = true }; - var teardown = new Thread (() => { try { barrier.SignalAndWait (); teardownThreadId = Environment.CurrentManagedThreadId; codec.Dispose (); } catch (Exception ex) { teardownError = ex; } }) { IsBackground = true }; - dispose.Start (); - teardown.Start (); - var completed = dispose.Join (TimeSpan.FromSeconds (30)); - completed &= teardown.Join (TimeSpan.FromSeconds (30)); - Assert.True (completed, "Dispose/teardown race did not complete in time."); - if (disposeError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (disposeError).Throw (); - if (teardownError != null) System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (teardownError).Throw (); - - // The ignored public Dispose never closes the stream; the codec owner-teardown is the - // real disposer. Proven by the closing thread being the teardown task, not the public - // Dispose task — DisposeCount==1 alone could not distinguish which path won. - Assert.Equal (teardownThreadId, dotnet.FirstDisposeThreadId); - - // Keep both wrappers rooted across the race assertion so a GC + finalizer - // can never flip FirstDisposeThreadId from the finalizer thread mid-assert. - GC.KeepAlive (stream); - GC.KeepAlive (dotnet); - } - } - finally - { - // Idempotent: redundant if the racing thread already tore the codec down. - codec.Dispose(); - } - - // Ignored public Dispose + single owner teardown => one .NET stream close, one latch flip. - Assert.Equal(1, dotnet.DisposeCount); - Assert.Equal(1, stream.fromNative); - Assert.True(stream.IsDisposed); - SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); - } - } - - [SkippableFact] - public unsafe void NonSeekableStreamReparentedToCodecTearsDownNestedStreamExactlyOnce() - { - // A NON-SEEKABLE stream routes SKCodec.Create through SKFrontBufferedManagedStream, which - // holds a private *inner* SKManagedStream wrapping the user's .NET stream. Only the OUTER - // front-buffered wrapper is reparented onto the codec (OwnsHandle=false, IgnorePublicDispose - // =true); the inner SKManagedStream is UNTOUCHED (still owns its handle, still forwards public - // Dispose). The user's racing fb.Dispose() must be a no-op, and codec teardown must walk - // fb -> inner -> .NET stream and close the .NET stream EXACTLY once. We supply the inner - // explicitly (public ctor) so both wrappers' deregistration is asserted without reflection. - var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); - - var dotnet = new NonSeekableGatedCountingStream(bytes); - var inner = new SKManagedStream(dotnet, true); - var innerHandle = inner.Handle; - var fb = new SKFrontBufferedManagedStream(inner, SKCodec.MinBufferedBytesNeeded, true); - var fbHandle = fb.Handle; - var codec = SKCodec.Create(fb); - try - { - Assert.NotNull(codec); - - // Only the outer front-buffered wrapper is reparented; the inner stays a normal owner. - Assert.False(fb.OwnsHandle); - Assert.True(fb.IgnorePublicDispose); - Assert.True(inner.OwnsHandle); - Assert.False(inner.IgnorePublicDispose); - - Assert.Equal(SKCodecResult.Success, codec.GetPixels(out _)); - - // The user still holds fb and (wrongly) disposes it: it is reparented, so this is a no-op. - fb.Dispose(); - Assert.False(fb.IsDisposed); - Assert.False(inner.IsDisposed); - Assert.Equal(0, dotnet.DisposeCount); - } - finally - { - codec.Dispose(); - } - - // Codec teardown is the sole disposer: fb -> inner -> .NET stream, closed exactly once. - Assert.True(fb.IsDisposed); - Assert.True(inner.IsDisposed); - Assert.Equal(1, dotnet.DisposeCount); - SKHandleDictionaryTestHelpers.AssertDeregistered(fbHandle, fb); - SKHandleDictionaryTestHelpers.AssertDeregistered(innerHandle, inner); - } - - [SkippableFact] - public unsafe void PublicDisposeWhileFrontBufferedCodecReadIsInFlightIsIgnored() - { - SkipOnPlatform(IsBrowser, "WASM is single-threaded; this test requires real OS threads"); - - // DETERMINISTIC in-flight-read race for the NESTED non-seekable graph. Park the native lazy - // read inside the underlying .NET Stream.Read() (reached through fb's front buffer -> inner - // SKManagedStream), fire the reparented fb's public Dispose() while that read is provably - // in-flight, then release. The ignored public Dispose must not close anything mid-read, so - // GetPixels succeeds and the .NET stream is closed exactly once by codec teardown. - var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); - - using var readEntered = new ManualResetEventSlim(false); - using var releaseRead = new ManualResetEventSlim(false); - - var dotnet = new NonSeekableGatedCountingStream(bytes); - var fb = new SKFrontBufferedManagedStream(dotnet, SKCodec.MinBufferedBytesNeeded, true); - var fbHandle = fb.Handle; - var codec = SKCodec.Create(fb); - Task reader = null; - try - { - Assert.False(fb.OwnsHandle); - Assert.True(fb.IgnorePublicDispose); - - // Arm only AFTER Create() so the gate trips on a GetPixels read past the front buffer, not - // on the header bytes buffered during format detection. - dotnet.Arm(readEntered, releaseRead); - - var result = SKCodecResult.InternalError; - reader = Task.Run(() => result = codec.GetPixels(out _)); - - Assert.True(readEntered.Wait(TimeSpan.FromSeconds(30)), "Native lazy read never reached the underlying non-seekable stream."); - - // The native read is parked inside the underlying Read(); a public Dispose now must no-op. - fb.Dispose(); - Assert.False(fb.IsDisposed); - Assert.Equal(0, dotnet.DisposeCount); - - releaseRead.Set(); - Assert.True(reader.Wait(TimeSpan.FromSeconds(30)), "GetPixels did not complete after the read was released."); - - Assert.Equal(SKCodecResult.Success, result); - Assert.False(fb.IsDisposed); - Assert.Equal(0, dotnet.DisposeCount); - } - finally - { - releaseRead.Set(); - try { reader?.Wait(TimeSpan.FromSeconds(30)); } catch { } - codec.Dispose(); - } - - // The codec is the sole disposer; the underlying .NET stream was closed exactly once. - Assert.True(fb.IsDisposed); - Assert.Equal(1, dotnet.DisposeCount); - SKHandleDictionaryTestHelpers.AssertDeregistered(fbHandle, fb); - } - - [SkippableFact] - public unsafe void InvalidNonSeekableStreamFailedCodecCreateClosesNestedStreamExactlyOnce() - { - // FAILURE path for the nested non-seekable graph: when the codec cannot be created, Create - // still transfers ownership to a null native object, which disposes the front-buffered - // wrapper immediately. That teardown must walk fb -> inner -> .NET stream and close it - // exactly once, and both wrappers must deregister — no leak, no double-free. - var bytes = new byte[256]; - for (var i = 0; i < bytes.Length; i++) - bytes[i] = (byte)(i + 1); - - var dotnet = new NonSeekableGatedCountingStream(bytes); - var inner = new SKManagedStream(dotnet, true); - var innerHandle = inner.Handle; - var fb = new SKFrontBufferedManagedStream(inner, SKCodec.MinBufferedBytesNeeded, true); - var fbHandle = fb.Handle; - - var codec = SKCodec.Create(fb, out var result); - - Assert.Null(codec); - Assert.NotEqual(SKCodecResult.Success, result); - - // The failed Create disposed fb immediately, which tore down the whole nested chain once. - Assert.True(fb.IsDisposed); - Assert.True(inner.IsDisposed); - Assert.Equal(1, dotnet.DisposeCount); - SKHandleDictionaryTestHelpers.AssertDeregistered(fbHandle, fb); - SKHandleDictionaryTestHelpers.AssertDeregistered(innerHandle, inner); - } - - [SkippableFact] - public unsafe void InvalidSeekableManagedStreamFailedCodecCreateClosesStreamExactlyOnce() - { - // FAILURE path for the FLAT seekable graph (no front-buffering): a seekable managed Stream - // of invalid bytes is wrapped in a single SKManagedStream. When the codec cannot be created, - // Create transfers ownership to a null native object, so RevokeOwnership(null) disposes the - // wrapper immediately. That teardown must close the .NET stream EXACTLY once and deregister - // the wrapper — no leak, no double-free. Complements the non-seekable/front-buffered failure - // test above, which exercises a different (nested) teardown graph. - var bytes = new byte[256]; - for (var i = 0; i < bytes.Length; i++) - bytes[i] = (byte)(i + 1); - - var dotnet = new GatedCountingStream(bytes); - var stream = new SKManagedStream(dotnet, true); - var handle = stream.Handle; - - Assert.True(stream.OwnsHandle); - Assert.True(SKObject.GetInstance(handle, out _)); - - var codec = SKCodec.Create(stream, out var result); - - Assert.Null(codec); - Assert.NotEqual(SKCodecResult.Success, result); - - // Failed Create disposed the flat wrapper, closing the .NET stream exactly once. - Assert.True(stream.IsDisposed); - Assert.Equal(1, dotnet.DisposeCount); - SKHandleDictionaryTestHelpers.AssertDeregistered(handle, stream); - } - - [SkippableFact] - public unsafe void FrontBufferedCodecWithoutOwnershipDoesNotCloseUnderlyingStream() - { - // disposeUnderlyingStream:false — the user keeps ownership of the .NET stream. The nested - // SKManagedStream is still disposed by codec teardown, but it must NOT close the user's .NET - // stream. We then close it ourselves to prove the count is driven solely by the user, not the - // codec chain. - var bytes = File.ReadAllBytes(Path.Combine(PathToImages, "color-wheel.png")); - - var dotnet = new NonSeekableGatedCountingStream(bytes); - var fb = new SKFrontBufferedManagedStream(dotnet, SKCodec.MinBufferedBytesNeeded, false); - var fbHandle = fb.Handle; - var codec = SKCodec.Create(fb); - try - { - Assert.NotNull(codec); - Assert.False(fb.OwnsHandle); - Assert.True(fb.IgnorePublicDispose); - Assert.Equal(SKCodecResult.Success, codec.GetPixels(out _)); - } - finally - { - codec.Dispose(); - } - - // Codec teardown disposed the wrappers but, lacking ownership, left the .NET stream open. - Assert.True(fb.IsDisposed); - SKHandleDictionaryTestHelpers.AssertDeregistered(fbHandle, fb); - Assert.Equal(0, dotnet.DisposeCount); - - // The user is the only owner: closing now is the first and only close. - dotnet.Dispose(); - Assert.Equal(1, dotnet.DisposeCount); - } - - // A seekable .NET Stream over a byte buffer that (a) counts how many times it is disposed and - // (b) can park the FIRST read after Arm() until released — used to drive a deterministic - // "public Dispose while the native lazy read is in-flight" race and to count exact teardown. - private sealed class GatedCountingStream : Stream - { - private readonly MemoryStream inner; - private ManualResetEventSlim readEntered; - private ManualResetEventSlim releaseRead; - private int gateArmed; - private int disposeCount; - private int firstDisposeThreadId; - - public GatedCountingStream(byte[] bytes) => inner = new MemoryStream(bytes, writable: false); - - public int DisposeCount => Volatile.Read(ref disposeCount); - - // Managed thread id of whoever closed the .NET stream first. Lets a test prove the codec - // owner-teardown — not the ignored public Dispose — was the actual disposer. - public int FirstDisposeThreadId => Volatile.Read(ref firstDisposeThreadId); - - public void Arm(ManualResetEventSlim entered, ManualResetEventSlim release) - { - readEntered = entered; - releaseRead = release; - Volatile.Write(ref gateArmed, 1); - } - - public override int Read(byte[] buffer, int offset, int count) - { - if (Interlocked.CompareExchange(ref gateArmed, 0, 1) == 1) - { - readEntered.Set(); - releaseRead.Wait(); - } - return inner.Read(buffer, offset, count); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - if (Interlocked.Increment(ref disposeCount) == 1) - Volatile.Write(ref firstDisposeThreadId, Environment.CurrentManagedThreadId); - // Actually close the backing buffer: a premature close during an in-flight read - // then surfaces as an ObjectDisposedException from the parked inner.Read. - inner.Dispose(); - } - base.Dispose(disposing); - } - - public override bool CanRead => true; - public override bool CanSeek => true; - public override bool CanWrite => false; - public override long Length => inner.Length; - public override long Position { get => inner.Position; set => inner.Position = value; } - public override long Seek(long offset, SeekOrigin origin) => inner.Seek(offset, origin); - public override void Flush() => inner.Flush(); - public override void SetLength(long value) => throw new NotSupportedException(); - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - } - - // A NON-SEEKABLE .NET Stream over a byte buffer that (a) counts how many times it is disposed, - // (b) records the disposing thread id, and (c) can park the first read after Arm() until - // released. Non-seekable so SKCodec.Create routes it through the SKFrontBufferedManagedStream - // (nested SKManagedStream) path — a different teardown graph from the flat seekable wrapper. - private sealed class NonSeekableGatedCountingStream : Stream - { - private readonly MemoryStream inner; - private ManualResetEventSlim readEntered; - private ManualResetEventSlim releaseRead; - private int gateArmed; - private int disposeCount; - private int firstDisposeThreadId; - - public NonSeekableGatedCountingStream(byte[] bytes) => inner = new MemoryStream(bytes, writable: false); - - public int DisposeCount => Volatile.Read(ref disposeCount); - - public int FirstDisposeThreadId => Volatile.Read(ref firstDisposeThreadId); - - public void Arm(ManualResetEventSlim entered, ManualResetEventSlim release) - { - readEntered = entered; - releaseRead = release; - Volatile.Write(ref gateArmed, 1); - } - - public override int Read(byte[] buffer, int offset, int count) - { - if (Interlocked.CompareExchange(ref gateArmed, 0, 1) == 1) - { - readEntered.Set(); - releaseRead.Wait(); - } - return inner.Read(buffer, offset, count); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - if (Interlocked.Increment(ref disposeCount) == 1) - Volatile.Write(ref firstDisposeThreadId, Environment.CurrentManagedThreadId); - inner.Dispose(); - } - base.Dispose(disposing); - } - - public override bool CanRead => true; - public override bool CanSeek => false; - public override bool CanWrite => false; - public override long Length => throw new NotSupportedException(); - public override long Position { get => inner.Position; set => throw new NotSupportedException(); } - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - public override void Flush() => throw new NotSupportedException(); - public override void SetLength(long value) => throw new NotSupportedException(); - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - } - // A MemoryStream that records disposal and rejects (rather than silently swallowing) // any write that arrives after the stream has been closed — so a premature close in // the owned-wstream teardown ordering would surface as WroteAfterClose / an exception. diff --git a/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs b/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs index b5aaf436627..16dd73b1c39 100644 --- a/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs +++ b/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs @@ -4,6 +4,7 @@ namespace SkiaSharp.Tests { // Proposed gap-filling tests for PR #4080. + [Collection (HandleDictionaryThreadingCollection.Name)] public class SKSingletonConcurrencyTest : SKTest { // PROBABILISTIC deadlock canary (NOT a deterministic race proof). diff --git a/tests/Tests/SkiaSharp/SKSingletonInitTest.cs b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs index 70b0e4efeb3..6541076143a 100644 --- a/tests/Tests/SkiaSharp/SKSingletonInitTest.cs +++ b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs @@ -8,6 +8,12 @@ namespace SkiaSharp.Tests { + // Serialized: SameColorSpaceCreatedDifferentWaysAreTheSameObject asserts the EXACT native refcount + // of the process-wide srgb-linear singleton, which any parallel test creating/decoding into + // srgb-linear would transiently perturb. Running in the DisableParallelization phase removes that + // interference. The remaining identity/flag/cold-start tests here are race-immune and microsecond-fast, + // so serializing the whole (small) class costs nothing meaningful. + [Collection (HandleDictionaryThreadingCollection.Name)] public class SKSingletonInitTest : SKTest { // --- Singleton identity + dispose-protected flag --- @@ -30,6 +36,47 @@ public void SKColorSpaceSrgbLinearReturnsSameInstanceAndIsDisposeProtected() Assert.True(a.IgnorePublicDispose); } + [SkippableFact] + public void SameColorSpaceCreatedDifferentWaysAreTheSameObject() + { + // Drain pending finalizers first so a srgb-linear holder created during the earlier parallel + // phase cannot decrement the native refcount below baseline mid-test. Combined with the + // serialized collection (no concurrent test increments), the count is then deterministic. + CollectGarbage(); + + // get the first instance of the sRGB Linear and capture the steady-state refcount + var colorspace1 = SKColorSpace.CreateSrgbLinear(); + Assert.True(colorspace1.IgnorePublicDispose); + var baselineRefCount = colorspace1.GetReferenceCount(); + + // create a new one with the same parameters, which will return the same instance + var colorspace2 = SKColorSpace.CreateRgb(SKColorSpaceTransferFn.Linear, SKColorSpaceXyz.Srgb); + Assert.True(colorspace2.IgnorePublicDispose); + Assert.Same(colorspace1, colorspace2); + Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); + Assert.Equal(baselineRefCount, colorspace2.GetReferenceCount()); + + // create a different one manually, which will return a new instance + var colorspace3 = SKColorSpace.CreateRgb( + new SKColorSpaceTransferFn { A = 0.6f, B = 0.5f, C = 0.4f, D = 0.3f, E = 0.2f, F = 0.1f }, + SKColorSpaceXyz.Identity); + Assert.NotSame(colorspace1, colorspace3); + Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); + Assert.Equal(baselineRefCount, colorspace2.GetReferenceCount()); + + colorspace3.Dispose(); + Assert.True(colorspace3.IsDisposed); + Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); + + colorspace2.Dispose(); + Assert.False(colorspace2.IsDisposed); + Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); + + colorspace1.Dispose(); + Assert.False(colorspace1.IsDisposed); + Assert.Equal(baselineRefCount, colorspace1.GetReferenceCount()); + } + [SkippableFact] public void SKDataEmptyReturnsSameInstanceAndIsDisposeProtected() { From 14459759615f155e11e05fa676cc115e5d94130a Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Tue, 9 Jun 2026 10:23:58 -0400 Subject: [PATCH 25/29] Hold native-wrapper arguments alive across P/Invoke calls A managed wrapper passed as an argument to a native call (e.g. the path in SKPathMeasure(path), data in SKCodec.Create, etc.) could be garbage-collected and finalized DURING that native call, freeing the underlying native object while the in-flight call still read it -> use-after-free / process crash on net48 under GC pressure. Add GC.KeepAlive(arg) after the consuming native call across the SkiaSharp and HarfBuzzSharp wrappers, and fix the demonstrated SKPathMeasure case. Includes Pr4080UseAfterFreeTest, which forces the race deterministically with a GC/finalizer hammer thread (crashes on the unfixed build, passes when fixed). Co-Authored-By: Claude Opus 4.8 --- binding/HarfBuzzSharp/Blob.cs | 26 ++- binding/HarfBuzzSharp/Buffer.cs | 122 +++++++--- binding/HarfBuzzSharp/Face.cs | 135 ++++++++--- binding/HarfBuzzSharp/Font.cs | 78 +++++-- binding/HarfBuzzSharp/FontFunctions.cs | 12 +- binding/HarfBuzzSharp/UnicodeFunctions.cs | 44 +++- binding/SkiaSharp/GRBackendRenderTarget.cs | 45 +++- binding/SkiaSharp/GRBackendTexture.cs | 38 +++- binding/SkiaSharp/GRContext.cs | 57 +++-- binding/SkiaSharp/GRGlInterface.cs | 14 +- binding/SkiaSharp/GRRecordingContext.cs | 35 ++- binding/SkiaSharp/SKBitmap.cs | 72 ++++-- binding/SkiaSharp/SKCanvas.cs | 143 ++++++++++-- binding/SkiaSharp/SKCodec.cs | 99 +++++--- binding/SkiaSharp/SKColorFilter.cs | 10 +- binding/SkiaSharp/SKColorSpace.cs | 57 +++-- binding/SkiaSharp/SKData.cs | 29 ++- binding/SkiaSharp/SKDocument.cs | 26 ++- binding/SkiaSharp/SKDrawable.cs | 27 ++- binding/SkiaSharp/SKFont.cs | 141 +++++++++--- binding/SkiaSharp/SKFontManager.cs | 28 ++- binding/SkiaSharp/SKFontStyleSet.cs | 11 +- binding/SkiaSharp/SKGraphics.cs | 5 +- binding/SkiaSharp/SKImage.cs | 169 ++++++++++---- binding/SkiaSharp/SKImageFilter.cs | 191 ++++++++++++---- binding/SkiaSharp/SKMaskFilter.cs | 4 +- binding/SkiaSharp/SKNWayCanvas.cs | 2 + binding/SkiaSharp/SKOverdrawCanvas.cs | 1 + binding/SkiaSharp/SKPaint.cs | 214 ++++++++++++++---- binding/SkiaSharp/SKPath.cs | 102 ++++++--- binding/SkiaSharp/SKPathBuilder.cs | 166 ++++++++++---- binding/SkiaSharp/SKPathEffect.cs | 18 +- binding/SkiaSharp/SKPathMeasure.cs | 27 ++- binding/SkiaSharp/SKPicture.cs | 47 +++- binding/SkiaSharp/SKPictureRecorder.cs | 20 +- binding/SkiaSharp/SKPixmap.cs | 87 +++++-- binding/SkiaSharp/SKRegion.cs | 124 +++++++--- binding/SkiaSharp/SKRoundRect.cs | 35 ++- binding/SkiaSharp/SKRuntimeEffect.cs | 20 +- binding/SkiaSharp/SKShader.cs | 63 ++++-- binding/SkiaSharp/SKStream.cs | 141 ++++++++---- binding/SkiaSharp/SKString.cs | 3 +- binding/SkiaSharp/SKSurface.cs | 84 +++++-- binding/SkiaSharp/SKTextBlob.cs | 35 ++- binding/SkiaSharp/SKTypeface.cs | 119 ++++++++-- binding/SkiaSharp/SKWebpEncoder.cs | 9 +- .../Tests/SkiaSharp/Pr4080UseAfterFreeTest.cs | 82 +++++++ 47 files changed, 2323 insertions(+), 694 deletions(-) create mode 100644 tests/Tests/SkiaSharp/Pr4080UseAfterFreeTest.cs diff --git a/binding/HarfBuzzSharp/Blob.cs b/binding/HarfBuzzSharp/Blob.cs index 12888988c05..10626b239b8 100644 --- a/binding/HarfBuzzSharp/Blob.cs +++ b/binding/HarfBuzzSharp/Blob.cs @@ -36,13 +36,31 @@ protected override void DisposeHandler () } } - public int Length => (int)HarfBuzzApi.hb_blob_get_length (Handle); + public int Length { + get { + var r = (int)HarfBuzzApi.hb_blob_get_length (Handle); + return r; + } + } - public int FaceCount => (int)HarfBuzzApi.hb_face_count (Handle); + public int FaceCount { + get { + var r = (int)HarfBuzzApi.hb_face_count (Handle); + return r; + } + } - public bool IsImmutable => HarfBuzzApi.hb_blob_is_immutable (Handle); + public bool IsImmutable { + get { + var r = HarfBuzzApi.hb_blob_is_immutable (Handle); + return r; + } + } - public void MakeImmutable () => HarfBuzzApi.hb_blob_make_immutable (Handle); + public void MakeImmutable () + { + HarfBuzzApi.hb_blob_make_immutable (Handle); + } public unsafe Stream AsStream () { diff --git a/binding/HarfBuzzSharp/Buffer.cs b/binding/HarfBuzzSharp/Buffer.cs index f34c24b1392..33055f2acf0 100644 --- a/binding/HarfBuzzSharp/Buffer.cs +++ b/binding/HarfBuzzSharp/Buffer.cs @@ -22,59 +22,110 @@ public Buffer () } public ContentType ContentType { - get => HarfBuzzApi.hb_buffer_get_content_type (Handle); - set => HarfBuzzApi.hb_buffer_set_content_type (Handle, value); + get { + var r = HarfBuzzApi.hb_buffer_get_content_type (Handle); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_content_type (Handle, value); + } } public Direction Direction { - get => HarfBuzzApi.hb_buffer_get_direction (Handle); - set => HarfBuzzApi.hb_buffer_set_direction (Handle, value); + get { + var r = HarfBuzzApi.hb_buffer_get_direction (Handle); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_direction (Handle, value); + } } public Language Language { - get => new Language (HarfBuzzApi.hb_buffer_get_language (Handle)); - set => HarfBuzzApi.hb_buffer_set_language (Handle, value.Handle); + get { + var r = new Language (HarfBuzzApi.hb_buffer_get_language (Handle)); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_language (Handle, value.Handle); + GC.KeepAlive (value); + } } public BufferFlags Flags { - get => HarfBuzzApi.hb_buffer_get_flags (Handle); - set => HarfBuzzApi.hb_buffer_set_flags (Handle, value); + get { + var r = HarfBuzzApi.hb_buffer_get_flags (Handle); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_flags (Handle, value); + } } public ClusterLevel ClusterLevel { - get => HarfBuzzApi.hb_buffer_get_cluster_level (Handle); - set => HarfBuzzApi.hb_buffer_set_cluster_level (Handle, value); + get { + var r = HarfBuzzApi.hb_buffer_get_cluster_level (Handle); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_cluster_level (Handle, value); + } } public uint ReplacementCodepoint { - get => HarfBuzzApi.hb_buffer_get_replacement_codepoint (Handle); - set => HarfBuzzApi.hb_buffer_set_replacement_codepoint (Handle, value); + get { + var r = HarfBuzzApi.hb_buffer_get_replacement_codepoint (Handle); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_replacement_codepoint (Handle, value); + } } public uint InvisibleGlyph { - get => HarfBuzzApi.hb_buffer_get_invisible_glyph (Handle); - set => HarfBuzzApi.hb_buffer_set_invisible_glyph (Handle, value); + get { + var r = HarfBuzzApi.hb_buffer_get_invisible_glyph (Handle); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_invisible_glyph (Handle, value); + } } public Script Script { - get => HarfBuzzApi.hb_buffer_get_script (Handle); - set => HarfBuzzApi.hb_buffer_set_script (Handle, value); + get { + var r = HarfBuzzApi.hb_buffer_get_script (Handle); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_script (Handle, value); + } } public int Length { - get => (int)HarfBuzzApi.hb_buffer_get_length (Handle); - set => HarfBuzzApi.hb_buffer_set_length (Handle, (uint)value); + get { + var r = (int)HarfBuzzApi.hb_buffer_get_length (Handle); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_length (Handle, (uint)value); + } } public UnicodeFunctions UnicodeFunctions { - get => new UnicodeFunctions (HarfBuzzApi.hb_buffer_get_unicode_funcs (Handle)); - set => HarfBuzzApi.hb_buffer_set_unicode_funcs (Handle, value.Handle); + get { + var r = new UnicodeFunctions (HarfBuzzApi.hb_buffer_get_unicode_funcs (Handle)); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_unicode_funcs (Handle, value.Handle); + GC.KeepAlive (value); + } } public GlyphInfo[] GlyphInfos { get { var array = GetGlyphInfoSpan ().ToArray (); - GC.KeepAlive (this); return array; } } @@ -82,7 +133,6 @@ public GlyphInfo[] GlyphInfos { public GlyphPosition[] GlyphPositions { get { var array = GetGlyphPositionSpan ().ToArray (); - GC.KeepAlive (this); return array; } } @@ -262,9 +312,15 @@ public void GuessSegmentProperties () HarfBuzzApi.hb_buffer_guess_segment_properties (Handle); } - public void ClearContents () => HarfBuzzApi.hb_buffer_clear_contents (Handle); + public void ClearContents () + { + HarfBuzzApi.hb_buffer_clear_contents (Handle); + } - public void Reset () => HarfBuzzApi.hb_buffer_reset (Handle); + public void Reset () + { + HarfBuzzApi.hb_buffer_reset (Handle); + } public void Append (Buffer buffer) => Append (buffer, 0, -1); @@ -276,6 +332,7 @@ public void Append (Buffer buffer, int start, int end) throw new InvalidOperationException ("ContentType must be of same type."); HarfBuzzApi.hb_buffer_append (Handle, buffer.Handle, (uint)start, (uint)(end == -1 ? buffer.Length : end)); + GC.KeepAlive (buffer); } public void NormalizeGlyphs () @@ -288,12 +345,20 @@ public void NormalizeGlyphs () HarfBuzzApi.hb_buffer_normalize_glyphs (Handle); } - public void Reverse () => HarfBuzzApi.hb_buffer_reverse (Handle); + public void Reverse () + { + HarfBuzzApi.hb_buffer_reverse (Handle); + } - public void ReverseRange (int start, int end) => + public void ReverseRange (int start, int end) + { HarfBuzzApi.hb_buffer_reverse_range (Handle, (uint)start, (uint)(end == -1 ? Length : end)); + } - public void ReverseClusters () => HarfBuzzApi.hb_buffer_reverse_clusters (Handle); + public void ReverseClusters () + { + HarfBuzzApi.hb_buffer_reverse_clusters (Handle); + } public string SerializeGlyphs () => SerializeGlyphs (0, -1, null, SerializeFormat.Text, SerializeFlag.Default); @@ -340,6 +405,8 @@ public unsafe string SerializeGlyphs (int start, int end, Font font, SerializeFo builder.Append (Marshal.PtrToStringAnsi ((IntPtr)pinned.Pointer, (int)consumed)); } + GC.KeepAlive (font); + return builder.ToString (); } @@ -357,6 +424,7 @@ public void DeserializeGlyphs (string data, Font font, SerializeFormat format) throw new InvalidOperationException ("ContentType must not be Glyphs."); HarfBuzzApi.hb_buffer_deserialize_glyphs (Handle, data, -1, null, font?.Handle ?? IntPtr.Zero, format); + GC.KeepAlive (font); } protected override void Dispose (bool disposing) => diff --git a/binding/HarfBuzzSharp/Face.cs b/binding/HarfBuzzSharp/Face.cs index 1dc21ff2a6d..2687a80728a 100644 --- a/binding/HarfBuzzSharp/Face.cs +++ b/binding/HarfBuzzSharp/Face.cs @@ -27,6 +27,7 @@ public Face (Blob blob, int index) } Handle = HarfBuzzApi.hb_face_create (blob.Handle, (uint)index); + GC.KeepAlive (blob); } public Face (GetTableDelegate getTable) @@ -52,18 +53,33 @@ internal Face (IntPtr handle) } public int Index { - get => (int)HarfBuzzApi.hb_face_get_index (Handle); - set => HarfBuzzApi.hb_face_set_index (Handle, (uint)value); + get { + var r = (int)HarfBuzzApi.hb_face_get_index (Handle); + return r; + } + set { + HarfBuzzApi.hb_face_set_index (Handle, (uint)value); + } } public int UnitsPerEm { - get => (int)HarfBuzzApi.hb_face_get_upem (Handle); - set => HarfBuzzApi.hb_face_set_upem (Handle, (uint)value); + get { + var r = (int)HarfBuzzApi.hb_face_get_upem (Handle); + return r; + } + set { + HarfBuzzApi.hb_face_set_upem (Handle, (uint)value); + } } public int GlyphCount { - get => (int)HarfBuzzApi.hb_face_get_glyph_count (Handle); - set => HarfBuzzApi.hb_face_set_glyph_count (Handle, (uint)value); + get { + var r = (int)HarfBuzzApi.hb_face_get_glyph_count (Handle); + return r; + } + set { + HarfBuzzApi.hb_face_set_glyph_count (Handle, (uint)value); + } } public unsafe Tag[] Tables { @@ -78,26 +94,47 @@ public unsafe Tag[] Tables { } } - public Blob ReferenceTable (Tag table) => - new Blob (HarfBuzzApi.hb_face_reference_table (Handle, table)); + public Blob ReferenceTable (Tag table) + { + var r = new Blob (HarfBuzzApi.hb_face_reference_table (Handle, table)); + return r; + } - public bool IsImmutable => HarfBuzzApi.hb_face_is_immutable (Handle); + public bool IsImmutable { + get { + var r = HarfBuzzApi.hb_face_is_immutable (Handle); + return r; + } + } - public void MakeImmutable () => HarfBuzzApi.hb_face_make_immutable (Handle); + public void MakeImmutable () + { + HarfBuzzApi.hb_face_make_immutable (Handle); + } // Variable font support - public bool HasVariationData => HarfBuzzApi.hb_ot_var_has_data (Handle); + public bool HasVariationData { + get { + var r = HarfBuzzApi.hb_ot_var_has_data (Handle); + return r; + } + } - public int VariationAxisCount => - (int)HarfBuzzApi.hb_ot_var_get_axis_count (Handle); + public int VariationAxisCount { + get { + var r = (int)HarfBuzzApi.hb_ot_var_get_axis_count (Handle); + return r; + } + } public OpenTypeVarAxisInfo[] VariationAxisInfos { get { var count = HarfBuzzApi.hb_ot_var_get_axis_count (Handle); - if (count == 0) + if (count == 0) { return Array.Empty (); + } var axes = new OpenTypeVarAxisInfo[(int)count]; fixed (OpenTypeVarAxisInfo* ptr = axes) { @@ -120,25 +157,32 @@ public bool TryFindVariationAxis (Tag tag, out OpenTypeVarAxisInfo axisInfo) { axisInfo = default; fixed (OpenTypeVarAxisInfo* ptr = &axisInfo) { - return HarfBuzzApi.hb_ot_var_find_axis_info (Handle, tag, ptr); + var r = HarfBuzzApi.hb_ot_var_find_axis_info (Handle, tag, ptr); + return r; } } - public int NamedInstanceCount => - (int)HarfBuzzApi.hb_ot_var_get_named_instance_count (Handle); + public int NamedInstanceCount { + get { + var r = (int)HarfBuzzApi.hb_ot_var_get_named_instance_count (Handle); + return r; + } + } public OpenTypeNameId GetNamedInstanceSubfamilyNameId (int instanceIndex) { if (instanceIndex < 0) throw new ArgumentOutOfRangeException (nameof (instanceIndex)); - return HarfBuzzApi.hb_ot_var_named_instance_get_subfamily_name_id (Handle, (uint)instanceIndex); + var r = HarfBuzzApi.hb_ot_var_named_instance_get_subfamily_name_id (Handle, (uint)instanceIndex); + return r; } public OpenTypeNameId GetNamedInstancePostScriptNameId (int instanceIndex) { if (instanceIndex < 0) throw new ArgumentOutOfRangeException (nameof (instanceIndex)); - return HarfBuzzApi.hb_ot_var_named_instance_get_postscript_name_id (Handle, (uint)instanceIndex); + var r = HarfBuzzApi.hb_ot_var_named_instance_get_postscript_name_id (Handle, (uint)instanceIndex); + return r; } public int GetNamedInstanceDesignCoordsCount (int instanceIndex) @@ -147,7 +191,8 @@ public int GetNamedInstanceDesignCoordsCount (int instanceIndex) throw new ArgumentOutOfRangeException (nameof (instanceIndex)); // Return value is the total number of design coordinates - return (int)HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, null, null); + var r = (int)HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, null, null); + return r; } public float[] GetNamedInstanceDesignCoords (int instanceIndex) @@ -157,8 +202,9 @@ public float[] GetNamedInstanceDesignCoords (int instanceIndex) // Return value is the total number of design coordinates var totalCoords = (int)HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, null, null); - if (totalCoords == 0) + if (totalCoords == 0) { return Array.Empty (); + } uint coordsLength = (uint)totalCoords; var coords = new float[totalCoords]; @@ -182,9 +228,19 @@ public int GetNamedInstanceDesignCoords (int instanceIndex, Span coords) // Color font / palette support - public bool HasPalettes => HarfBuzzApi.hb_ot_color_has_palettes (Handle); + public bool HasPalettes { + get { + var r = HarfBuzzApi.hb_ot_color_has_palettes (Handle); + return r; + } + } - public int PaletteCount => (int)HarfBuzzApi.hb_ot_color_palette_get_count (Handle); + public int PaletteCount { + get { + var r = (int)HarfBuzzApi.hb_ot_color_palette_get_count (Handle); + return r; + } + } public HBColor[] GetPaletteColors (int paletteIndex) { @@ -192,8 +248,9 @@ public HBColor[] GetPaletteColors (int paletteIndex) throw new ArgumentOutOfRangeException (nameof (paletteIndex)); var totalColors = (int)HarfBuzzApi.hb_ot_color_palette_get_colors (Handle, (uint)paletteIndex, 0, null, null); - if (totalColors == 0) + if (totalColors == 0) { return Array.Empty (); + } uint count = (uint)totalColors; var colors = new HBColor[totalColors]; @@ -219,28 +276,46 @@ public OpenTypeColorPaletteFlags GetPaletteFlags (int paletteIndex) { if (paletteIndex < 0) throw new ArgumentOutOfRangeException (nameof (paletteIndex)); - return HarfBuzzApi.hb_ot_color_palette_get_flags (Handle, (uint)paletteIndex); + var r = HarfBuzzApi.hb_ot_color_palette_get_flags (Handle, (uint)paletteIndex); + return r; } public OpenTypeNameId GetPaletteNameId (int paletteIndex) { if (paletteIndex < 0) throw new ArgumentOutOfRangeException (nameof (paletteIndex)); - return HarfBuzzApi.hb_ot_color_palette_get_name_id (Handle, (uint)paletteIndex); + var r = HarfBuzzApi.hb_ot_color_palette_get_name_id (Handle, (uint)paletteIndex); + return r; } public OpenTypeNameId GetPaletteColorNameId (int colorIndex) { if (colorIndex < 0) throw new ArgumentOutOfRangeException (nameof (colorIndex)); - return HarfBuzzApi.hb_ot_color_palette_color_get_name_id (Handle, (uint)colorIndex); + var r = HarfBuzzApi.hb_ot_color_palette_color_get_name_id (Handle, (uint)colorIndex); + return r; } - public bool HasColorLayers => HarfBuzzApi.hb_ot_color_has_layers (Handle); + public bool HasColorLayers { + get { + var r = HarfBuzzApi.hb_ot_color_has_layers (Handle); + return r; + } + } - public bool HasColorPng => HarfBuzzApi.hb_ot_color_has_png (Handle); + public bool HasColorPng { + get { + var r = HarfBuzzApi.hb_ot_color_has_png (Handle); + return r; + } + } - public bool HasColorSvg => HarfBuzzApi.hb_ot_color_has_svg (Handle); + public bool HasColorSvg { + get { + var r = HarfBuzzApi.hb_ot_color_has_svg (Handle); + return r; + } + } protected override void Dispose (bool disposing) => diff --git a/binding/HarfBuzzSharp/Font.cs b/binding/HarfBuzzSharp/Font.cs index 835bcc15e2f..926a466e875 100644 --- a/binding/HarfBuzzSharp/Font.cs +++ b/binding/HarfBuzzSharp/Font.cs @@ -19,6 +19,7 @@ public Font (Face face) throw new ArgumentNullException (nameof (face)); Handle = HarfBuzzApi.hb_font_create (face.Handle); + GC.KeepAlive (face); OpenTypeMetrics = new OpenTypeMetrics (this); } @@ -55,6 +56,7 @@ public void SetFontFunctions (FontFunctions fontFunctions, object fontData, Rele var container = new FontUserData (this, fontData); var ctx = DelegateProxies.CreateMultiUserData (destroy, container); HarfBuzzApi.hb_font_set_funcs (Handle, fontFunctions.Handle, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (fontFunctions); } public void GetScale (out int xScale, out int yScale) @@ -65,20 +67,24 @@ public void GetScale (out int xScale, out int yScale) } } - public void SetScale (int xScale, int yScale) => + public void SetScale (int xScale, int yScale) + { HarfBuzzApi.hb_font_set_scale (Handle, xScale, yScale); + } public bool TryGetHorizontalFontExtents (out FontExtents extents) { fixed (FontExtents* e = &extents) { - return HarfBuzzApi.hb_font_get_h_extents (Handle, e); + var r = HarfBuzzApi.hb_font_get_h_extents (Handle, e); + return r; } } public bool TryGetVerticalFontExtents (out FontExtents extents) { fixed (FontExtents* e = &extents) { - return HarfBuzzApi.hb_font_get_v_extents (Handle, e); + var r = HarfBuzzApi.hb_font_get_v_extents (Handle, e); + return r; } } @@ -88,7 +94,8 @@ public bool TryGetNominalGlyph (int unicode, out uint glyph) => public bool TryGetNominalGlyph (uint unicode, out uint glyph) { fixed (uint* g = &glyph) { - return HarfBuzzApi.hb_font_get_nominal_glyph (Handle, unicode, g); + var r = HarfBuzzApi.hb_font_get_nominal_glyph (Handle, unicode, g); + return r; } } @@ -98,7 +105,8 @@ public bool TryGetVariationGlyph (int unicode, out uint glyph) => public bool TryGetVariationGlyph (uint unicode, out uint glyph) { fixed (uint* g = &glyph) { - return HarfBuzzApi.hb_font_get_variation_glyph (Handle, unicode, 0, g); + var r = HarfBuzzApi.hb_font_get_variation_glyph (Handle, unicode, 0, g); + return r; } } @@ -108,15 +116,22 @@ public bool TryGetVariationGlyph (int unicode, uint variationSelector, out uint public bool TryGetVariationGlyph (uint unicode, uint variationSelector, out uint glyph) { fixed (uint* g = &glyph) { - return HarfBuzzApi.hb_font_get_variation_glyph (Handle, unicode, variationSelector, g); + var r = HarfBuzzApi.hb_font_get_variation_glyph (Handle, unicode, variationSelector, g); + return r; } } - public int GetHorizontalGlyphAdvance (uint glyph) => - HarfBuzzApi.hb_font_get_glyph_h_advance (Handle, glyph); + public int GetHorizontalGlyphAdvance (uint glyph) + { + var r = HarfBuzzApi.hb_font_get_glyph_h_advance (Handle, glyph); + return r; + } - public int GetVerticalGlyphAdvance (uint glyph) => - HarfBuzzApi.hb_font_get_glyph_v_advance (Handle, glyph); + public int GetVerticalGlyphAdvance (uint glyph) + { + var r = HarfBuzzApi.hb_font_get_glyph_v_advance (Handle, glyph); + return r; + } public unsafe int[] GetHorizontalGlyphAdvances (ReadOnlySpan glyphs) { @@ -158,7 +173,8 @@ public bool TryGetHorizontalGlyphOrigin (uint glyph, out int xOrigin, out int yO { fixed (int* x = &xOrigin) fixed (int* y = &yOrigin) { - return HarfBuzzApi.hb_font_get_glyph_h_origin (Handle, glyph, x, y); + var r = HarfBuzzApi.hb_font_get_glyph_h_origin (Handle, glyph, x, y); + return r; } } @@ -166,17 +182,22 @@ public bool TryGetVerticalGlyphOrigin (uint glyph, out int xOrigin, out int yOri { fixed (int* x = &xOrigin) fixed (int* y = &yOrigin) { - return HarfBuzzApi.hb_font_get_glyph_v_origin (Handle, glyph, x, y); + var r = HarfBuzzApi.hb_font_get_glyph_v_origin (Handle, glyph, x, y); + return r; } } - public int GetHorizontalGlyphKerning (uint leftGlyph, uint rightGlyph) => - HarfBuzzApi.hb_font_get_glyph_h_kerning (Handle, leftGlyph, rightGlyph); + public int GetHorizontalGlyphKerning (uint leftGlyph, uint rightGlyph) + { + var r = HarfBuzzApi.hb_font_get_glyph_h_kerning (Handle, leftGlyph, rightGlyph); + return r; + } public bool TryGetGlyphExtents (uint glyph, out GlyphExtents extents) { fixed (GlyphExtents* e = &extents) { - return HarfBuzzApi.hb_font_get_glyph_extents (Handle, glyph, e); + var r = HarfBuzzApi.hb_font_get_glyph_extents (Handle, glyph, e); + return r; } } @@ -184,7 +205,8 @@ public bool TryGetGlyphContourPoint (uint glyph, uint pointIndex, out int x, out { fixed (int* xPtr = &x) fixed (int* yPtr = &y) { - return HarfBuzzApi.hb_font_get_glyph_contour_point (Handle, glyph, pointIndex, xPtr, yPtr); + var r = HarfBuzzApi.hb_font_get_glyph_contour_point (Handle, glyph, pointIndex, xPtr, yPtr); + return r; } } @@ -209,7 +231,8 @@ public unsafe bool TryGetGlyphName (uint glyph, out string name) public bool TryGetGlyphFromName (string name, out uint glyph) { fixed (uint* g = &glyph) { - return HarfBuzzApi.hb_font_get_glyph_from_name (Handle, name, name.Length, g); + var r = HarfBuzzApi.hb_font_get_glyph_from_name (Handle, name, name.Length, g); + return r; } } @@ -225,7 +248,8 @@ public bool TryGetGlyph (int unicode, uint variationSelector, out uint glyph) => public bool TryGetGlyph (uint unicode, uint variationSelector, out uint glyph) { fixed (uint* g = &glyph) { - return HarfBuzzApi.hb_font_get_glyph (Handle, unicode, variationSelector, g); + var r = HarfBuzzApi.hb_font_get_glyph (Handle, unicode, variationSelector, g); + return r; } } @@ -266,7 +290,8 @@ public bool TryGetGlyphContourPointForOrigin (uint glyph, uint pointIndex, Direc { fixed (int* xPtr = &x) fixed (int* yPtr = &y) { - return HarfBuzzApi.hb_font_get_glyph_contour_point_for_origin (Handle, glyph, pointIndex, direction, xPtr, yPtr); + var r = HarfBuzzApi.hb_font_get_glyph_contour_point_for_origin (Handle, glyph, pointIndex, direction, xPtr, yPtr); + return r; } } @@ -287,7 +312,8 @@ public unsafe string GlyphToString (uint glyph) public bool TryGetGlyphFromString (string s, out uint glyph) { fixed (uint* g = &glyph) { - return HarfBuzzApi.hb_font_glyph_from_string (Handle, s, -1, g); + var r = HarfBuzzApi.hb_font_glyph_from_string (Handle, s, -1, g); + return r; } } @@ -319,8 +345,9 @@ public int[] VariationCoordsNormalized get { uint length; var ptr = HarfBuzzApi.hb_font_get_var_coords_normalized (Handle, &length); - if (length == 0 || ptr == null) + if (length == 0 || ptr == null) { return Array.Empty (); + } var count = (int)length; var coords = new int[count]; @@ -334,8 +361,9 @@ public int GetVariationCoordsNormalized (Span coords) { uint length; var ptr = HarfBuzzApi.hb_font_get_var_coords_normalized (Handle, &length); - if (length == 0 || ptr == null) + if (length == 0 || ptr == null) { return 0; + } var count = Math.Min ((int)length, coords.Length); for (int i = 0; i < count; i++) @@ -350,8 +378,10 @@ public void SetVariationNamedInstance (int instanceIndex) HarfBuzzApi.hb_font_set_var_named_instance (Handle, (uint)instanceIndex); } - public void SetFunctionsOpenType () => + public void SetFunctionsOpenType () + { HarfBuzzApi.hb_ot_font_set_funcs (Handle); + } public void Shape (Buffer buffer, params Feature[] features) => Shape (buffer, features, null); @@ -390,6 +420,8 @@ public void Shape (Buffer buffer, IReadOnlyList features, IReadOnlyList sPtr); } + GC.KeepAlive (buffer); + if (shapersPtrs != null) { for (var i = 0; i < shapersPtrs.Length; i++) { if (shapersPtrs[i] != null) diff --git a/binding/HarfBuzzSharp/FontFunctions.cs b/binding/HarfBuzzSharp/FontFunctions.cs index 7b48aa9935e..7f6e2353c39 100644 --- a/binding/HarfBuzzSharp/FontFunctions.cs +++ b/binding/HarfBuzzSharp/FontFunctions.cs @@ -21,9 +21,17 @@ internal FontFunctions (IntPtr handle) public static FontFunctions Empty => emptyFontFunctions.Value; - public bool IsImmutable => HarfBuzzApi.hb_font_funcs_is_immutable (Handle); + public bool IsImmutable { + get { + var r = HarfBuzzApi.hb_font_funcs_is_immutable (Handle); + return r; + } + } - public void MakeImmutable () => HarfBuzzApi.hb_font_funcs_make_immutable (Handle); + public void MakeImmutable () + { + HarfBuzzApi.hb_font_funcs_make_immutable (Handle); + } public void SetHorizontalFontExtentsDelegate (FontExtentsDelegate del, ReleaseDelegate destroy = null) { diff --git a/binding/HarfBuzzSharp/UnicodeFunctions.cs b/binding/HarfBuzzSharp/UnicodeFunctions.cs index 459f86cfc88..7432259c19d 100644 --- a/binding/HarfBuzzSharp/UnicodeFunctions.cs +++ b/binding/HarfBuzzSharp/UnicodeFunctions.cs @@ -34,27 +34,49 @@ public UnicodeFunctions (UnicodeFunctions parent) : base (IntPtr.Zero) public UnicodeFunctions Parent { get; } - public bool IsImmutable => HarfBuzzApi.hb_unicode_funcs_is_immutable (Handle); + public bool IsImmutable { + get { + var r = HarfBuzzApi.hb_unicode_funcs_is_immutable (Handle); + return r; + } + } - public void MakeImmutable () => HarfBuzzApi.hb_unicode_funcs_make_immutable (Handle); + public void MakeImmutable () + { + HarfBuzzApi.hb_unicode_funcs_make_immutable (Handle); + } public UnicodeCombiningClass GetCombiningClass (int unicode) => GetCombiningClass ((uint)unicode); - public UnicodeCombiningClass GetCombiningClass (uint unicode) => - HarfBuzzApi.hb_unicode_combining_class (Handle, unicode); + public UnicodeCombiningClass GetCombiningClass (uint unicode) + { + var r = HarfBuzzApi.hb_unicode_combining_class (Handle, unicode); + return r; + } public UnicodeGeneralCategory GetGeneralCategory (int unicode) => GetGeneralCategory ((uint)unicode); - public UnicodeGeneralCategory GetGeneralCategory (uint unicode) => - HarfBuzzApi.hb_unicode_general_category (Handle, unicode); + public UnicodeGeneralCategory GetGeneralCategory (uint unicode) + { + var r = HarfBuzzApi.hb_unicode_general_category (Handle, unicode); + return r; + } public int GetMirroring (int unicode) => (int)GetMirroring ((uint)unicode); - public uint GetMirroring (uint unicode) => HarfBuzzApi.hb_unicode_mirroring (Handle, unicode); + public uint GetMirroring (uint unicode) + { + var r = HarfBuzzApi.hb_unicode_mirroring (Handle, unicode); + return r; + } public Script GetScript (int unicode) => GetScript ((uint)unicode); - public Script GetScript (uint unicode) => HarfBuzzApi.hb_unicode_script (Handle, unicode); + public Script GetScript (uint unicode) + { + var r = HarfBuzzApi.hb_unicode_script (Handle, unicode); + return r; + } public bool TryCompose (int a, int b, out int ab) { @@ -68,7 +90,8 @@ public bool TryCompose (int a, int b, out int ab) public bool TryCompose (uint a, uint b, out uint ab) { fixed (uint* abPtr = &ab) { - return HarfBuzzApi.hb_unicode_compose (Handle, a, b, abPtr); + var r = HarfBuzzApi.hb_unicode_compose (Handle, a, b, abPtr); + return r; } } @@ -87,7 +110,8 @@ public bool TryDecompose (uint ab, out uint a, out uint b) { fixed (uint* aPtr = &a) fixed (uint* bPtr = &b) { - return HarfBuzzApi.hb_unicode_decompose (Handle, ab, aPtr, bPtr); + var r = HarfBuzzApi.hb_unicode_decompose (Handle, ab, aPtr, bPtr); + return r; } } diff --git a/binding/SkiaSharp/GRBackendRenderTarget.cs b/binding/SkiaSharp/GRBackendRenderTarget.cs index 0edf8eac0bf..c345f35288a 100644 --- a/binding/SkiaSharp/GRBackendRenderTarget.cs +++ b/binding/SkiaSharp/GRBackendRenderTarget.cs @@ -91,12 +91,42 @@ protected override void Dispose (bool disposing) => protected override void DisposeNative () => SkiaApi.gr_backendrendertarget_delete (Handle); - public bool IsValid => SkiaApi.gr_backendrendertarget_is_valid (Handle); - public int Width => SkiaApi.gr_backendrendertarget_get_width (Handle); - public int Height => SkiaApi.gr_backendrendertarget_get_height (Handle); - public int SampleCount => SkiaApi.gr_backendrendertarget_get_samples (Handle); - public int StencilBits => SkiaApi.gr_backendrendertarget_get_stencils (Handle); - public GRBackend Backend => SkiaApi.gr_backendrendertarget_get_backend (Handle).FromNative (); + public bool IsValid { + get { + var result = SkiaApi.gr_backendrendertarget_is_valid (Handle); + return result; + } + } + public int Width { + get { + var result = SkiaApi.gr_backendrendertarget_get_width (Handle); + return result; + } + } + public int Height { + get { + var result = SkiaApi.gr_backendrendertarget_get_height (Handle); + return result; + } + } + public int SampleCount { + get { + var result = SkiaApi.gr_backendrendertarget_get_samples (Handle); + return result; + } + } + public int StencilBits { + get { + var result = SkiaApi.gr_backendrendertarget_get_stencils (Handle); + return result; + } + } + public GRBackend Backend { + get { + var result = SkiaApi.gr_backendrendertarget_get_backend (Handle).FromNative (); + return result; + } + } public SKSizeI Size => new SKSizeI (Width, Height); public SKRectI Rect => new SKRectI (0, 0, Width, Height); @@ -106,7 +136,8 @@ public GRGlFramebufferInfo GetGlFramebufferInfo () => public bool GetGlFramebufferInfo (out GRGlFramebufferInfo glInfo) { fixed (GRGlFramebufferInfo* g = &glInfo) { - return SkiaApi.gr_backendrendertarget_get_gl_framebufferinfo (Handle, g); + var result = SkiaApi.gr_backendrendertarget_get_gl_framebufferinfo (Handle, g); + return result; } } } diff --git a/binding/SkiaSharp/GRBackendTexture.cs b/binding/SkiaSharp/GRBackendTexture.cs index 531c4c6eb19..4f920bbbd30 100644 --- a/binding/SkiaSharp/GRBackendTexture.cs +++ b/binding/SkiaSharp/GRBackendTexture.cs @@ -76,11 +76,36 @@ protected override void Dispose (bool disposing) => protected override void DisposeNative () => SkiaApi.gr_backendtexture_delete (Handle); - public bool IsValid => SkiaApi.gr_backendtexture_is_valid (Handle); - public int Width => SkiaApi.gr_backendtexture_get_width (Handle); - public int Height => SkiaApi.gr_backendtexture_get_height (Handle); - public bool HasMipMaps => SkiaApi.gr_backendtexture_has_mipmaps (Handle); - public GRBackend Backend => SkiaApi.gr_backendtexture_get_backend (Handle).FromNative (); + public bool IsValid { + get { + var result = SkiaApi.gr_backendtexture_is_valid (Handle); + return result; + } + } + public int Width { + get { + var result = SkiaApi.gr_backendtexture_get_width (Handle); + return result; + } + } + public int Height { + get { + var result = SkiaApi.gr_backendtexture_get_height (Handle); + return result; + } + } + public bool HasMipMaps { + get { + var result = SkiaApi.gr_backendtexture_has_mipmaps (Handle); + return result; + } + } + public GRBackend Backend { + get { + var result = SkiaApi.gr_backendtexture_get_backend (Handle).FromNative (); + return result; + } + } public SKSizeI Size => new SKSizeI (Width, Height); public SKRectI Rect => new SKRectI (0, 0, Width, Height); @@ -90,7 +115,8 @@ public GRGlTextureInfo GetGlTextureInfo () => public bool GetGlTextureInfo (out GRGlTextureInfo glInfo) { fixed (GRGlTextureInfo* g = &glInfo) { - return SkiaApi.gr_backendtexture_get_gl_textureinfo (Handle, g); + var result = SkiaApi.gr_backendtexture_get_gl_textureinfo (Handle, g); + return result; } } } diff --git a/binding/SkiaSharp/GRContext.cs b/binding/SkiaSharp/GRContext.cs index 91aa9dc228c..326c3553079 100644 --- a/binding/SkiaSharp/GRContext.cs +++ b/binding/SkiaSharp/GRContext.cs @@ -38,10 +38,14 @@ public static GRContext CreateGl (GRGlInterface backendContext, GRContextOptions var ctx = backendContext == null ? IntPtr.Zero : backendContext.Handle; if (options == null) { - return GetObject (SkiaApi.gr_direct_context_make_gl (ctx)); + var context = GetObject (SkiaApi.gr_direct_context_make_gl (ctx)); + GC.KeepAlive (backendContext); + return context; } else { var opts = options.ToNative (); - return GetObject (SkiaApi.gr_direct_context_make_gl_with_options (ctx, &opts)); + var context = GetObject (SkiaApi.gr_direct_context_make_gl_with_options (ctx, &opts)); + GC.KeepAlive (backendContext); + return context; } } @@ -103,7 +107,12 @@ public static GRContext CreateMetal (GRMtlBackendContext backendContext, GRConte public override GRBackend Backend => base.Backend; - public override bool IsAbandoned => SkiaApi.gr_direct_context_is_abandoned (Handle); + public override bool IsAbandoned { + get { + var result = SkiaApi.gr_direct_context_is_abandoned (Handle); + return result; + } + } public void AbandonContext (bool releaseResources = false) { @@ -113,11 +122,16 @@ public void AbandonContext (bool releaseResources = false) SkiaApi.gr_direct_context_abandon_context (Handle); } - public long GetResourceCacheLimit () => - (long)SkiaApi.gr_direct_context_get_resource_cache_limit (Handle); + public long GetResourceCacheLimit () + { + var result = (long)SkiaApi.gr_direct_context_get_resource_cache_limit (Handle); + return result; + } - public void SetResourceCacheLimit (long maxResourceBytes) => + public void SetResourceCacheLimit (long maxResourceBytes) + { SkiaApi.gr_direct_context_set_resource_cache_limit (Handle, (IntPtr)maxResourceBytes); + } public void GetResourceCacheUsage (out int maxResources, out long maxResourceBytes) { @@ -134,8 +148,10 @@ public void ResetContext (GRGlBackendState state) => public void ResetContext (GRBackendState state = GRBackendState.All) => ResetContext ((uint)state); - public void ResetContext (uint state) => + public void ResetContext (uint state) + { SkiaApi.gr_direct_context_reset_context (Handle, state); + } public void Flush () => Flush (true); @@ -147,8 +163,10 @@ public void Flush (bool submit, bool synchronous = false) SkiaApi.gr_direct_context_flush (Handle); } - public void Submit (bool synchronous = false) => + public void Submit (bool synchronous = false) + { SkiaApi.gr_direct_context_submit (Handle, synchronous); + } public void Flush (SKImage image) { @@ -157,6 +175,7 @@ public void Flush (SKImage image) } SkiaApi.gr_direct_context_flush_image (Handle, image.Handle); + GC.KeepAlive (image); } public void Flush (SKSurface surface) @@ -166,25 +185,37 @@ public void Flush (SKSurface surface) } SkiaApi.gr_direct_context_flush_surface (Handle, surface.Handle); + GC.KeepAlive (surface); } public new int GetMaxSurfaceSampleCount (SKColorType colorType) => base.GetMaxSurfaceSampleCount (colorType); - public void DumpMemoryStatistics (SKTraceMemoryDump dump) => + public void DumpMemoryStatistics (SKTraceMemoryDump dump) + { SkiaApi.gr_direct_context_dump_memory_statistics (Handle, dump?.Handle ?? throw new ArgumentNullException (nameof (dump))); + GC.KeepAlive (dump); + } - public void PurgeResources () => + public void PurgeResources () + { SkiaApi.gr_direct_context_free_gpu_resources (Handle); + } - public void PurgeUnusedResources (long milliseconds) => + public void PurgeUnusedResources (long milliseconds) + { SkiaApi.gr_direct_context_perform_deferred_cleanup (Handle, milliseconds); + } - public void PurgeUnlockedResources (bool scratchResourcesOnly) => + public void PurgeUnlockedResources (bool scratchResourcesOnly) + { SkiaApi.gr_direct_context_purge_unlocked_resources (Handle, scratchResourcesOnly); + } - public void PurgeUnlockedResources (long bytesToPurge, bool preferScratchResources) => + public void PurgeUnlockedResources (long bytesToPurge, bool preferScratchResources) + { SkiaApi.gr_direct_context_purge_unlocked_resources_bytes (Handle, (IntPtr)bytesToPurge, preferScratchResources); + } internal new static GRContext GetObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => GetOrAddObject (handle, owns, unrefExisting, (h, o) => new GRContext (h, o)); diff --git a/binding/SkiaSharp/GRGlInterface.cs b/binding/SkiaSharp/GRGlInterface.cs index d0a023812f6..4fb6d00bd1b 100644 --- a/binding/SkiaSharp/GRGlInterface.cs +++ b/binding/SkiaSharp/GRGlInterface.cs @@ -108,11 +108,17 @@ public static GRGlInterface CreateEvas (IntPtr evas) // - public bool Validate () => - SkiaApi.gr_glinterface_validate (Handle); + public bool Validate () + { + var result = SkiaApi.gr_glinterface_validate (Handle); + return result; + } - public bool HasExtension (string extension) => - SkiaApi.gr_glinterface_has_extension (Handle, extension); + public bool HasExtension (string extension) + { + var result = SkiaApi.gr_glinterface_has_extension (Handle, extension); + return result; + } // diff --git a/binding/SkiaSharp/GRRecordingContext.cs b/binding/SkiaSharp/GRRecordingContext.cs index 3a40694362d..3c32a47e81d 100644 --- a/binding/SkiaSharp/GRRecordingContext.cs +++ b/binding/SkiaSharp/GRRecordingContext.cs @@ -11,16 +11,39 @@ internal GRRecordingContext (IntPtr h, bool owns) { } - public virtual GRBackend Backend => SkiaApi.gr_recording_context_get_backend (Handle).FromNative (); + public virtual GRBackend Backend { + get { + var result = SkiaApi.gr_recording_context_get_backend (Handle).FromNative (); + return result; + } + } - public virtual bool IsAbandoned => SkiaApi.gr_recording_context_is_abandoned (Handle); + public virtual bool IsAbandoned { + get { + var result = SkiaApi.gr_recording_context_is_abandoned (Handle); + return result; + } + } - public int MaxTextureSize => SkiaApi.gr_recording_context_max_texture_size (Handle); + public int MaxTextureSize { + get { + var result = SkiaApi.gr_recording_context_max_texture_size (Handle); + return result; + } + } - public int MaxRenderTargetSize => SkiaApi.gr_recording_context_max_render_target_size (Handle); + public int MaxRenderTargetSize { + get { + var result = SkiaApi.gr_recording_context_max_render_target_size (Handle); + return result; + } + } - public int GetMaxSurfaceSampleCount (SKColorType colorType) => - SkiaApi.gr_recording_context_get_max_surface_sample_count_for_color_type (Handle, colorType.ToNative ()); + public int GetMaxSurfaceSampleCount (SKColorType colorType) + { + var result = SkiaApi.gr_recording_context_get_max_surface_sample_count_for_color_type (Handle, colorType.ToNative ()); + return result; + } internal static GRRecordingContext GetObject (IntPtr handle, bool owns = true, bool unrefExisting = true) { diff --git a/binding/SkiaSharp/SKBitmap.cs b/binding/SkiaSharp/SKBitmap.cs index cc4ed9f18c4..897fbeeb388 100644 --- a/binding/SkiaSharp/SKBitmap.cs +++ b/binding/SkiaSharp/SKBitmap.cs @@ -81,13 +81,15 @@ public bool TryAllocPixels (SKImageInfo info) public bool TryAllocPixels (SKImageInfo info, int rowBytes) { var cinfo = SKImageInfoNative.FromManaged (ref info); - return SkiaApi.sk_bitmap_try_alloc_pixels (Handle, &cinfo, (IntPtr)rowBytes); + var result = SkiaApi.sk_bitmap_try_alloc_pixels (Handle, &cinfo, (IntPtr)rowBytes); + return result; } public bool TryAllocPixels (SKImageInfo info, SKBitmapAllocFlags flags) { var cinfo = SKImageInfoNative.FromManaged (ref info); - return SkiaApi.sk_bitmap_try_alloc_pixels_with_flags (Handle, &cinfo, (uint)flags); + var result = SkiaApi.sk_bitmap_try_alloc_pixels_with_flags (Handle, &cinfo, (uint)flags); + return result; } // Reset @@ -118,14 +120,18 @@ public void Erase (SKColor color, SKRectI rect) // GetAddress - public IntPtr GetAddress (int x, int y) => - (IntPtr)SkiaApi.sk_bitmap_get_addr (Handle, x, y); + public IntPtr GetAddress (int x, int y) + { + var result = (IntPtr)SkiaApi.sk_bitmap_get_addr (Handle, x, y); + return result; + } // Pixels (color) public SKColor GetPixel (int x, int y) { - return SkiaApi.sk_bitmap_get_pixel_color (Handle, x, y); + var result = SkiaApi.sk_bitmap_get_pixel_color (Handle, x, y); + return result; } public void SetPixel (int x, int y, SKColor color) @@ -218,7 +224,9 @@ public bool ExtractSubset (SKBitmap destination, SKRectI subset) if (destination == null) { throw new ArgumentNullException (nameof (destination)); } - return SkiaApi.sk_bitmap_extract_subset (Handle, destination.Handle, &subset); + var result = SkiaApi.sk_bitmap_extract_subset (Handle, destination.Handle, &subset); + GC.KeepAlive (destination); + return result; } // ExtractAlpha @@ -244,13 +252,21 @@ public bool ExtractAlpha (SKBitmap destination, SKPaint paint, out SKPointI offs throw new ArgumentNullException (nameof (destination)); } fixed (SKPointI* o = &offset) { - return SkiaApi.sk_bitmap_extract_alpha (Handle, destination.Handle, paint == null ? IntPtr.Zero : paint.Handle, o); + var result = SkiaApi.sk_bitmap_extract_alpha (Handle, destination.Handle, paint == null ? IntPtr.Zero : paint.Handle, o); + GC.KeepAlive (destination); + GC.KeepAlive (paint); + return result; } } // properties - public bool ReadyToDraw => SkiaApi.sk_bitmap_ready_to_draw (Handle); + public bool ReadyToDraw { + get { + var result = SkiaApi.sk_bitmap_ready_to_draw (Handle); + return result; + } + } public SKImageInfo Info { get { @@ -285,11 +301,17 @@ public int BytesPerPixel { } public int RowBytes { - get { return (int)SkiaApi.sk_bitmap_get_row_bytes (Handle); } + get { + var result = (int)SkiaApi.sk_bitmap_get_row_bytes (Handle); + return result; + } } public int ByteCount { - get { return (int)SkiaApi.sk_bitmap_get_byte_count (Handle); } + get { + var result = (int)SkiaApi.sk_bitmap_get_byte_count (Handle); + return result; + } } // *Pixels* @@ -306,7 +328,8 @@ public Span GetPixelSpan (int x, int y) => public IntPtr GetPixels (out IntPtr length) { fixed (IntPtr* l = &length) { - return (IntPtr)SkiaApi.sk_bitmap_get_pixels (Handle, l); + var result = (IntPtr)SkiaApi.sk_bitmap_get_pixels (Handle, l); + return result; } } @@ -320,7 +343,6 @@ public void SetPixels (IntPtr pixels) public byte[] Bytes { get { var array = GetPixelSpan ().ToArray (); - GC.KeepAlive (this); return array; } } @@ -365,7 +387,10 @@ public bool IsEmpty { } public bool IsNull { - get { return SkiaApi.sk_bitmap_is_null (Handle); } + get { + var result = SkiaApi.sk_bitmap_is_null (Handle); + return result; + } } public bool DrawsNothing { @@ -373,7 +398,10 @@ public bool DrawsNothing { } public bool IsImmutable { - get { return SkiaApi.sk_bitmap_is_immutable (Handle); } + get { + var result = SkiaApi.sk_bitmap_is_immutable (Handle); + return result; + } } // DecodeBounds @@ -612,12 +640,15 @@ public bool InstallPixels (SKImageInfo info, IntPtr pixels, int rowBytes, SKBitm : releaseProc; DelegateProxies.Create (del, out _, out var ctx); var proxy = del is not null ? DelegateProxies.SKBitmapReleaseProxy : null; - return SkiaApi.sk_bitmap_install_pixels (Handle, &cinfo, (void*)pixels, (IntPtr)rowBytes, proxy, (void*)ctx); + var result = SkiaApi.sk_bitmap_install_pixels (Handle, &cinfo, (void*)pixels, (IntPtr)rowBytes, proxy, (void*)ctx); + return result; } public bool InstallPixels (SKPixmap pixmap) { - return SkiaApi.sk_bitmap_install_pixels_with_pixmap (Handle, pixmap.Handle); + var result = SkiaApi.sk_bitmap_install_pixels_with_pixmap (Handle, pixmap.Handle); + GC.KeepAlive (pixmap); + return result; } // NotifyPixelsChanged @@ -647,6 +678,7 @@ public bool PeekPixels (SKPixmap pixmap) throw new ArgumentNullException (nameof (pixmap)); } var result = SkiaApi.sk_bitmap_peek_pixels (Handle, pixmap.Handle); + GC.KeepAlive (pixmap); if (result) pixmap.pixelSource = this; return result; @@ -756,6 +788,7 @@ public bool Encode (SKWStream dst, SKEncodedImageFormat format, int quality) private void Swap (SKBitmap other) { SkiaApi.sk_bitmap_swap (Handle, other.Handle); + GC.KeepAlive (other); } // ToShader @@ -782,7 +815,10 @@ public SKShader ToShader (SKShaderTileMode tmx, SKShaderTileMode tmy, SKSampling public SKShader ToShader (SKShaderTileMode tmx, SKShaderTileMode tmy, SKFilterQuality quality, SKMatrix localMatrix) => ToShader (tmx, tmy, quality.ToSamplingOptions(), &localMatrix); - private SKShader ToShader (SKShaderTileMode tmx, SKShaderTileMode tmy, SKSamplingOptions sampling, SKMatrix* localMatrix) => - SKShader.GetObject (SkiaApi.sk_bitmap_make_shader (Handle, tmx, tmy, &sampling, localMatrix)); + private SKShader ToShader (SKShaderTileMode tmx, SKShaderTileMode tmy, SKSamplingOptions sampling, SKMatrix* localMatrix) + { + var result = SKShader.GetObject (SkiaApi.sk_bitmap_make_shader (Handle, tmx, tmy, &sampling, localMatrix)); + return result; + } } } diff --git a/binding/SkiaSharp/SKCanvas.cs b/binding/SkiaSharp/SKCanvas.cs index fee2faf9311..02c4971ea61 100644 --- a/binding/SkiaSharp/SKCanvas.cs +++ b/binding/SkiaSharp/SKCanvas.cs @@ -24,6 +24,7 @@ public SKCanvas (SKBitmap bitmap) if (bitmap == null) throw new ArgumentNullException (nameof (bitmap)); Handle = SkiaApi.sk_canvas_new_from_bitmap (bitmap.Handle); + GC.KeepAlive (bitmap); } protected override void Dispose (bool disposing) => @@ -32,14 +33,17 @@ protected override void Dispose (bool disposing) => protected override void DisposeNative () => SkiaApi.sk_canvas_destroy (Handle); - public void Discard () => + public void Discard () + { SkiaApi.sk_canvas_discard (Handle); + } // QuickReject public bool QuickReject (SKRect rect) { - return SkiaApi.sk_canvas_quick_reject (Handle, &rect); + var result = SkiaApi.sk_canvas_quick_reject (Handle, &rect); + return result; } public bool QuickReject (SKPath path) @@ -56,32 +60,49 @@ public int Save () { if (Handle == IntPtr.Zero) throw new ObjectDisposedException ("SKCanvas"); - return SkiaApi.sk_canvas_save (Handle); + var result = SkiaApi.sk_canvas_save (Handle); + return result; + } + + public int SaveLayer (SKRect limit, SKPaint? paint) + { + var result = SkiaApi.sk_canvas_save_layer (Handle, &limit, paint?.Handle ?? IntPtr.Zero); + GC.KeepAlive (paint); + return result; } - public int SaveLayer (SKRect limit, SKPaint? paint) => - SkiaApi.sk_canvas_save_layer (Handle, &limit, paint?.Handle ?? IntPtr.Zero); - - public int SaveLayer (SKPaint? paint) => - SkiaApi.sk_canvas_save_layer (Handle, null, paint?.Handle ?? IntPtr.Zero); + public int SaveLayer (SKPaint? paint) + { + var result = SkiaApi.sk_canvas_save_layer (Handle, null, paint?.Handle ?? IntPtr.Zero); + GC.KeepAlive (paint); + return result; + } public int SaveLayer (in SKCanvasSaveLayerRec rec) { var native = rec.ToNative (); - return SkiaApi.sk_canvas_save_layer_rec (Handle, &native); + var result = SkiaApi.sk_canvas_save_layer_rec (Handle, &native); + return result; } - public int SaveLayer () => - SkiaApi.sk_canvas_save_layer (Handle, null, IntPtr.Zero); + public int SaveLayer () + { + var result = SkiaApi.sk_canvas_save_layer (Handle, null, IntPtr.Zero); + return result; + } #nullable disable // DrawColor - public void DrawColor (SKColor color, SKBlendMode mode = SKBlendMode.Src) => + public void DrawColor (SKColor color, SKBlendMode mode = SKBlendMode.Src) + { SkiaApi.sk_canvas_draw_color (Handle, (uint)color, mode); + } - public void DrawColor (SKColorF color, SKBlendMode mode = SKBlendMode.Src) => + public void DrawColor (SKColorF color, SKBlendMode mode = SKBlendMode.Src) + { SkiaApi.sk_canvas_draw_color4f (Handle, color, mode); + } // DrawLine @@ -95,6 +116,7 @@ public void DrawLine (float x0, float y0, float x1, float y1, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_line (Handle, x0, y0, x1, y1, paint.Handle); + GC.KeepAlive (paint); } // Clear @@ -102,11 +124,15 @@ public void DrawLine (float x0, float y0, float x1, float y1, SKPaint paint) public void Clear () => Clear (SKColors.Empty); - public void Clear (SKColor color) => + public void Clear (SKColor color) + { SkiaApi.sk_canvas_clear (Handle, (uint)color); + } - public void Clear (SKColorF color) => + public void Clear (SKColorF color) + { SkiaApi.sk_canvas_clear_color4f (Handle, color); + } // Restore* @@ -255,6 +281,7 @@ public void ClipRoundRect (SKRoundRect rect, SKClipOperation operation = SKClipO throw new ArgumentNullException (nameof (rect)); SkiaApi.sk_canvas_clip_rrect_with_operation (Handle, rect.Handle, operation, antialias); + GC.KeepAlive (rect); } public void ClipPath (SKPath path, SKClipOperation operation = SKClipOperation.Intersect, bool antialias = false) @@ -263,6 +290,7 @@ public void ClipPath (SKPath path, SKClipOperation operation = SKClipOperation.I throw new ArgumentNullException (nameof (path)); SkiaApi.sk_canvas_clip_path_with_operation (Handle, path.Handle, operation, antialias); + GC.KeepAlive (path); } public void ClipRegion (SKRegion region, SKClipOperation operation = SKClipOperation.Intersect) @@ -271,6 +299,7 @@ public void ClipRegion (SKRegion region, SKClipOperation operation = SKClipOpera throw new ArgumentNullException (nameof (region)); SkiaApi.sk_canvas_clip_region (Handle, region.Handle, operation); + GC.KeepAlive (region); } public SKRect LocalClipBounds { @@ -287,21 +316,33 @@ public SKRectI DeviceClipBounds { } } - public bool IsClipEmpty => SkiaApi.sk_canvas_is_clip_empty (Handle); + public bool IsClipEmpty { + get { + var result = SkiaApi.sk_canvas_is_clip_empty (Handle); + return result; + } + } - public bool IsClipRect => SkiaApi.sk_canvas_is_clip_rect (Handle); + public bool IsClipRect { + get { + var result = SkiaApi.sk_canvas_is_clip_rect (Handle); + return result; + } + } public bool GetLocalClipBounds (out SKRect bounds) { fixed (SKRect* b = &bounds) { - return SkiaApi.sk_canvas_get_local_clip_bounds (Handle, b); + var result = SkiaApi.sk_canvas_get_local_clip_bounds (Handle, b); + return result; } } public bool GetDeviceClipBounds (out SKRectI bounds) { fixed (SKRectI* b = &bounds) { - return SkiaApi.sk_canvas_get_device_clip_bounds (Handle, b); + var result = SkiaApi.sk_canvas_get_device_clip_bounds (Handle, b); + return result; } } @@ -312,6 +353,7 @@ public void DrawPaint (SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_paint (Handle, paint.Handle); + GC.KeepAlive (paint); } // DrawRegion @@ -323,6 +365,8 @@ public void DrawRegion (SKRegion region, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_region (Handle, region.Handle, paint.Handle); + GC.KeepAlive (region); + GC.KeepAlive (paint); } // DrawRect @@ -337,6 +381,7 @@ public void DrawRect (SKRect rect, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_rect (Handle, &rect, paint.Handle); + GC.KeepAlive (paint); } // DrawRoundRect @@ -348,6 +393,8 @@ public void DrawRoundRect (SKRoundRect rect, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_rrect (Handle, rect.Handle, paint.Handle); + GC.KeepAlive (rect); + GC.KeepAlive (paint); } public void DrawRoundRect (float x, float y, float w, float h, float rx, float ry, SKPaint paint) @@ -360,6 +407,7 @@ public void DrawRoundRect (SKRect rect, float rx, float ry, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_round_rect (Handle, &rect, rx, ry, paint.Handle); + GC.KeepAlive (paint); } public void DrawRoundRect (SKRect rect, SKSize r, SKPaint paint) @@ -384,6 +432,7 @@ public void DrawOval (SKRect rect, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_oval (Handle, &rect, paint.Handle); + GC.KeepAlive (paint); } // DrawCircle @@ -393,6 +442,7 @@ public void DrawCircle (float cx, float cy, float radius, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_circle (Handle, cx, cy, radius, paint.Handle); + GC.KeepAlive (paint); } public void DrawCircle (SKPoint c, float radius, SKPaint paint) @@ -409,6 +459,8 @@ public void DrawPath (SKPath path, SKPaint paint) if (path == null) throw new ArgumentNullException (nameof (path)); SkiaApi.sk_canvas_draw_path (Handle, path.Handle, paint.Handle); + GC.KeepAlive (path); + GC.KeepAlive (paint); } // DrawPoints @@ -422,6 +474,7 @@ public void DrawPoints (SKPointMode mode, SKPoint[] points, SKPaint paint) fixed (SKPoint* p = points) { SkiaApi.sk_canvas_draw_points (Handle, mode, (IntPtr)points.Length, p, paint.Handle); } + GC.KeepAlive (paint); } // DrawPoint @@ -436,6 +489,7 @@ public void DrawPoint (float x, float y, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_point (Handle, x, y, paint.Handle); + GC.KeepAlive (paint); } public void DrawPoint (SKPoint p, SKColor color) @@ -472,6 +526,8 @@ public void DrawImage (SKImage image, float x, float y, SKSamplingOptions sampli if (image == null) throw new ArgumentNullException (nameof (image)); SkiaApi.sk_canvas_draw_image (Handle, image.Handle, x, y, &sampling, paint?.Handle ?? IntPtr.Zero); + GC.KeepAlive (image); + GC.KeepAlive (paint); } public void DrawImage (SKImage image, SKRect dest, SKPaint paint = null) @@ -499,6 +555,8 @@ private void DrawImage (SKImage image, SKRect* source, SKRect* dest, SKSamplingO if (image == null) throw new ArgumentNullException (nameof (image)); SkiaApi.sk_canvas_draw_image_rect (Handle, image.Handle, source, dest, &sampling, paint?.Handle ?? IntPtr.Zero); + GC.KeepAlive (image); + GC.KeepAlive (paint); } // DrawPicture @@ -520,6 +578,8 @@ public void DrawPicture (SKPicture picture, in SKMatrix matrix, SKPaint paint = throw new ArgumentNullException (nameof (picture)); fixed (SKMatrix* m = &matrix) SkiaApi.sk_canvas_draw_picture (Handle, picture.Handle, m, paint == null ? IntPtr.Zero : paint.Handle); + GC.KeepAlive (picture); + GC.KeepAlive (paint); } public void DrawPicture (SKPicture picture, SKPaint paint = null) @@ -527,6 +587,8 @@ public void DrawPicture (SKPicture picture, SKPaint paint = null) if (picture == null) throw new ArgumentNullException (nameof (picture)); SkiaApi.sk_canvas_draw_picture (Handle, picture.Handle, null, paint == null ? IntPtr.Zero : paint.Handle); + GC.KeepAlive (picture); + GC.KeepAlive (paint); } // DrawDrawable @@ -537,6 +599,7 @@ public void DrawDrawable (SKDrawable drawable, in SKMatrix matrix) throw new ArgumentNullException (nameof (drawable)); fixed (SKMatrix* m = &matrix) SkiaApi.sk_canvas_draw_drawable (Handle, drawable.Handle, m); + GC.KeepAlive (drawable); } public void DrawDrawable (SKDrawable drawable, float x, float y) @@ -616,6 +679,8 @@ public void DrawText (SKTextBlob text, float x, float y, SKPaint paint) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_text_blob (Handle, text.Handle, x, y, paint.Handle); + GC.KeepAlive (text); + GC.KeepAlive (paint); } // DrawText @@ -715,15 +780,23 @@ public void DrawTextOnPath (string text, SKPath path, SKPoint offset, bool warpG // Surface #nullable enable - public SKSurface? Surface => - SKSurface.GetObject (SkiaApi.sk_get_surface (Handle), owns: false, unrefExisting: false); + public SKSurface? Surface { + get { + var result = SKSurface.GetObject (SkiaApi.sk_get_surface (Handle), owns: false, unrefExisting: false); + return result; + } + } #nullable disable // Context #nullable enable - public GRRecordingContext? Context => - GRRecordingContext.GetObject (SkiaApi.sk_get_recording_context (Handle), owns: false, unrefExisting: false); + public GRRecordingContext? Context { + get { + var result = GRRecordingContext.GetObject (SkiaApi.sk_get_recording_context (Handle), owns: false, unrefExisting: false); + return result; + } + } #nullable disable // Flush @@ -741,11 +814,13 @@ public void DrawAnnotation (SKRect rect, string key, SKData value) fixed (byte* b = bytes) { SkiaApi.sk_canvas_draw_annotation (base.Handle, &rect, b, value == null ? IntPtr.Zero : value.Handle); } + GC.KeepAlive (value); } public void DrawUrlAnnotation (SKRect rect, SKData value) { SkiaApi.sk_canvas_draw_url_annotation (Handle, &rect, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); } public SKData DrawUrlAnnotation (SKRect rect, string value) @@ -758,6 +833,7 @@ public SKData DrawUrlAnnotation (SKRect rect, string value) public void DrawNamedDestinationAnnotation (SKPoint point, SKData value) { SkiaApi.sk_canvas_draw_named_destination_annotation (Handle, &point, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); } public SKData DrawNamedDestinationAnnotation (SKPoint point, string value) @@ -770,6 +846,7 @@ public SKData DrawNamedDestinationAnnotation (SKPoint point, string value) public void DrawLinkDestinationAnnotation (SKRect rect, SKData value) { SkiaApi.sk_canvas_draw_link_destination_annotation (Handle, &rect, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); } public SKData DrawLinkDestinationAnnotation (SKRect rect, string value) @@ -802,6 +879,8 @@ public void DrawImageNinePatch (SKImage image, SKRectI center, SKRect dst, SKFil throw new ArgumentException ("Center rectangle must be contained inside the image bounds.", nameof (center)); SkiaApi.sk_canvas_draw_image_nine (Handle, image.Handle, ¢er, &dst, filterMode, paint == null ? IntPtr.Zero : paint.Handle); + GC.KeepAlive (image); + GC.KeepAlive (paint); } // Draw*Lattice @@ -867,6 +946,8 @@ public void DrawImageLattice (SKImage image, SKLattice lattice, SKRect dst, SKFi } SkiaApi.sk_canvas_draw_image_lattice (Handle, image.Handle, &nativeLattice, &dst, filterMode, paint == null ? IntPtr.Zero : paint.Handle); } + GC.KeepAlive (image); + GC.KeepAlive (paint); } // *Matrix @@ -902,7 +983,12 @@ public SKMatrix44 TotalMatrix44 { // SaveCount - public int SaveCount => SkiaApi.sk_canvas_get_save_count (Handle); + public int SaveCount { + get { + var result = SkiaApi.sk_canvas_get_save_count (Handle); + return result; + } + } // DrawVertices @@ -937,6 +1023,8 @@ public void DrawVertices (SKVertices vertices, SKBlendMode mode, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_vertices (Handle, vertices.Handle, mode, paint.Handle); + GC.KeepAlive (vertices); + GC.KeepAlive (paint); } // DrawArc @@ -946,6 +1034,7 @@ public void DrawArc (SKRect oval, float startAngle, float sweepAngle, bool useCe if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_arc (Handle, &oval, startAngle, sweepAngle, useCenter, paint.Handle); + GC.KeepAlive (paint); } // DrawRoundRectDifference @@ -960,6 +1049,9 @@ public void DrawRoundRectDifference (SKRoundRect outer, SKRoundRect inner, SKPai throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_drrect (Handle, outer.Handle, inner.Handle, paint.Handle); + GC.KeepAlive (outer); + GC.KeepAlive (inner); + GC.KeepAlive (paint); } // DrawAtlas @@ -1001,6 +1093,8 @@ private void DrawAtlas (SKImage atlas, SKRect[] sprites, SKRotationScaleMatrix[] fixed (SKColor* c = colors) { SkiaApi.sk_canvas_draw_atlas (Handle, atlas.Handle, t, s, (uint*)c, transforms.Length, mode, &sampling, cullRect, paint?.Handle ?? IntPtr.Zero); } + GC.KeepAlive (atlas); + GC.KeepAlive (paint); } // DrawPatch @@ -1029,6 +1123,7 @@ public void DrawPatch (SKPoint[] cubics, SKColor[] colors, SKPoint[] texCoords, fixed (SKPoint* coords = texCoords) { SkiaApi.sk_canvas_draw_patch (Handle, cubes, (uint*)cols, coords, mode, paint.Handle); } + GC.KeepAlive (paint); } internal static SKCanvas GetObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => diff --git a/binding/SkiaSharp/SKCodec.cs b/binding/SkiaSharp/SKCodec.cs index 859f8b7825a..ec8002bf740 100644 --- a/binding/SkiaSharp/SKCodec.cs +++ b/binding/SkiaSharp/SKCodec.cs @@ -33,11 +33,19 @@ public SKImageInfo Info { } } - public SKEncodedOrigin EncodedOrigin => - SkiaApi.sk_codec_get_origin (Handle); + public SKEncodedOrigin EncodedOrigin { + get { + var result = SkiaApi.sk_codec_get_origin (Handle); + return result; + } + } - public SKEncodedImageFormat EncodedFormat => - SkiaApi.sk_codec_get_encoded_format (Handle); + public SKEncodedImageFormat EncodedFormat { + get { + var result = SkiaApi.sk_codec_get_encoded_format (Handle); + return result; + } + } public SKSizeI GetScaledDimensions (float desiredScale) { @@ -49,7 +57,8 @@ public SKSizeI GetScaledDimensions (float desiredScale) public bool GetValidSubset (ref SKRectI desiredSubset) { fixed (SKRectI* ds = &desiredSubset) { - return SkiaApi.sk_codec_get_valid_subset (Handle, ds); + var result = SkiaApi.sk_codec_get_valid_subset (Handle, ds); + return result; } } @@ -65,11 +74,19 @@ public byte[] Pixels { // frames - public int RepetitionCount => - SkiaApi.sk_codec_get_repetition_count (Handle); + public int RepetitionCount { + get { + var result = SkiaApi.sk_codec_get_repetition_count (Handle); + return result; + } + } - public int FrameCount => - SkiaApi.sk_codec_get_frame_count (Handle); + public int FrameCount { + get { + var result = SkiaApi.sk_codec_get_frame_count (Handle); + return result; + } + } public SKCodecFrameInfo[] FrameInfo { get { @@ -85,7 +102,8 @@ public SKCodecFrameInfo[] FrameInfo { public bool GetFrameInfo (int index, out SKCodecFrameInfo frameInfo) { fixed (SKCodecFrameInfo* f = &frameInfo) { - return SkiaApi.sk_codec_get_frame_info_for_index (Handle, index, f); + var result = SkiaApi.sk_codec_get_frame_info_for_index (Handle, index, f); + return result; } } @@ -133,7 +151,8 @@ public SKCodecResult GetPixels (SKImageInfo info, IntPtr pixels, int rowBytes, S subset = options.Subset.Value; nOptions.fSubset = ⊂ } - return SkiaApi.sk_codec_get_pixels (Handle, &nInfo, (void*)pixels, (IntPtr)rowBytes, &nOptions); + var result = SkiaApi.sk_codec_get_pixels (Handle, &nInfo, (void*)pixels, (IntPtr)rowBytes, &nOptions); + return result; } // incremental (start) @@ -156,13 +175,15 @@ public SKCodecResult StartIncrementalDecode (SKImageInfo info, IntPtr pixels, in nOptions.fSubset = ⊂ } - return SkiaApi.sk_codec_start_incremental_decode (Handle, &nInfo, (void*)pixels, (IntPtr)rowBytes, &nOptions); + var result = SkiaApi.sk_codec_start_incremental_decode (Handle, &nInfo, (void*)pixels, (IntPtr)rowBytes, &nOptions); + return result; } public SKCodecResult StartIncrementalDecode (SKImageInfo info, IntPtr pixels, int rowBytes) { var cinfo = SKImageInfoNative.FromManaged (ref info); - return SkiaApi.sk_codec_start_incremental_decode (Handle, &cinfo, (void*)pixels, (IntPtr)rowBytes, null); + var result = SkiaApi.sk_codec_start_incremental_decode (Handle, &cinfo, (void*)pixels, (IntPtr)rowBytes, null); + return result; } // incremental (step) @@ -170,12 +191,16 @@ public SKCodecResult StartIncrementalDecode (SKImageInfo info, IntPtr pixels, in public SKCodecResult IncrementalDecode (out int rowsDecoded) { fixed (int* r = &rowsDecoded) { - return SkiaApi.sk_codec_incremental_decode (Handle, r); + var result = SkiaApi.sk_codec_incremental_decode (Handle, r); + return result; } } - public SKCodecResult IncrementalDecode () => - SkiaApi.sk_codec_incremental_decode (Handle, null); + public SKCodecResult IncrementalDecode () + { + var result = SkiaApi.sk_codec_incremental_decode (Handle, null); + return result; + } // scanline (start) @@ -194,13 +219,15 @@ public SKCodecResult StartScanlineDecode (SKImageInfo info, SKCodecOptions optio nOptions.fSubset = ⊂ } - return SkiaApi.sk_codec_start_scanline_decode (Handle, &nInfo, &nOptions); + var result = SkiaApi.sk_codec_start_scanline_decode (Handle, &nInfo, &nOptions); + return result; } public SKCodecResult StartScanlineDecode (SKImageInfo info) { var cinfo = SKImageInfoNative.FromManaged (ref info); - return SkiaApi.sk_codec_start_scanline_decode (Handle, &cinfo, null); + var result = SkiaApi.sk_codec_start_scanline_decode (Handle, &cinfo, null); + return result; } // scanline (step) @@ -210,19 +237,35 @@ public int GetScanlines (IntPtr dst, int countLines, int rowBytes) if (dst == IntPtr.Zero) throw new ArgumentNullException (nameof (dst)); - return SkiaApi.sk_codec_get_scanlines (Handle, (void*)dst, countLines, (IntPtr)rowBytes); + var result = SkiaApi.sk_codec_get_scanlines (Handle, (void*)dst, countLines, (IntPtr)rowBytes); + return result; } - public bool SkipScanlines (int countLines) => - SkiaApi.sk_codec_skip_scanlines (Handle, countLines); + public bool SkipScanlines (int countLines) + { + var result = SkiaApi.sk_codec_skip_scanlines (Handle, countLines); + return result; + } - public SKCodecScanlineOrder ScanlineOrder => - SkiaApi.sk_codec_get_scanline_order (Handle); + public SKCodecScanlineOrder ScanlineOrder { + get { + var result = SkiaApi.sk_codec_get_scanline_order (Handle); + return result; + } + } - public int NextScanline => SkiaApi.sk_codec_next_scanline (Handle); + public int NextScanline { + get { + var result = SkiaApi.sk_codec_next_scanline (Handle); + return result; + } + } - public int GetOutputScanline (int inputScanline) => - SkiaApi.sk_codec_output_scanline (Handle, inputScanline); + public int GetOutputScanline (int inputScanline) + { + var result = SkiaApi.sk_codec_output_scanline (Handle, inputScanline); + return result; + } // create (streams) @@ -270,7 +313,9 @@ public static SKCodec Create (SKData data) if (data == null) throw new ArgumentNullException (nameof (data)); - return GetObject (SkiaApi.sk_codec_new_from_data (data.Handle)); + var codec = GetObject (SkiaApi.sk_codec_new_from_data (data.Handle)); + GC.KeepAlive (data); + return codec; } // utils diff --git a/binding/SkiaSharp/SKColorFilter.cs b/binding/SkiaSharp/SKColorFilter.cs index 5af57d4c25f..2b9e66ac205 100644 --- a/binding/SkiaSharp/SKColorFilter.cs +++ b/binding/SkiaSharp/SKColorFilter.cs @@ -52,7 +52,10 @@ public static SKColorFilter CreateCompose(SKColorFilter outer, SKColorFilter inn throw new ArgumentNullException(nameof(outer)); if (inner == null) throw new ArgumentNullException(nameof(inner)); - return GetObject (SkiaApi.sk_colorfilter_new_compose(outer.Handle, inner.Handle)); + var colorFilter = GetObject (SkiaApi.sk_colorfilter_new_compose(outer.Handle, inner.Handle)); + GC.KeepAlive (outer); + GC.KeepAlive (inner); + return colorFilter; } public static SKColorFilter CreateLerp(float weight, SKColorFilter filter0, SKColorFilter filter1) @@ -60,7 +63,10 @@ public static SKColorFilter CreateLerp(float weight, SKColorFilter filter0, SKCo _ = filter0 ?? throw new ArgumentNullException(nameof(filter0)); _ = filter1 ?? throw new ArgumentNullException(nameof(filter1)); - return GetObject (SkiaApi.sk_colorfilter_new_lerp(weight, filter0.Handle, filter1.Handle)); + var colorFilter = GetObject (SkiaApi.sk_colorfilter_new_lerp(weight, filter0.Handle, filter1.Handle)); + GC.KeepAlive (filter0); + GC.KeepAlive (filter1); + return colorFilter; } public static SKColorFilter CreateColorMatrix(float[] matrix) diff --git a/binding/SkiaSharp/SKColorSpace.cs b/binding/SkiaSharp/SKColorSpace.cs index c8e78f975fa..d1982d1b743 100644 --- a/binding/SkiaSharp/SKColorSpace.cs +++ b/binding/SkiaSharp/SKColorSpace.cs @@ -21,25 +21,41 @@ internal SKColorSpace (IntPtr handle, bool owns) { } - void ISKNonVirtualReferenceCounted.ReferenceNative () => + void ISKNonVirtualReferenceCounted.ReferenceNative () + { SkiaApi.sk_colorspace_ref (Handle); + } - void ISKNonVirtualReferenceCounted.UnreferenceNative () => + void ISKNonVirtualReferenceCounted.UnreferenceNative () + { SkiaApi.sk_colorspace_unref (Handle); + } protected override void Dispose (bool disposing) => base.Dispose (disposing); // properties - public bool GammaIsCloseToSrgb => - SkiaApi.sk_colorspace_gamma_close_to_srgb (Handle); + public bool GammaIsCloseToSrgb { + get { + var result = SkiaApi.sk_colorspace_gamma_close_to_srgb (Handle); + return result; + } + } - public bool GammaIsLinear => - SkiaApi.sk_colorspace_gamma_is_linear (Handle); + public bool GammaIsLinear { + get { + var result = SkiaApi.sk_colorspace_gamma_is_linear (Handle); + return result; + } + } - public bool IsSrgb => - SkiaApi.sk_colorspace_is_srgb (Handle); + public bool IsSrgb { + get { + var result = SkiaApi.sk_colorspace_is_srgb (Handle); + return result; + } + } public bool IsNumericalTransferFunction => GetNumericalTransferFunction (out _); @@ -51,7 +67,10 @@ public static bool Equal (SKColorSpace left, SKColorSpace right) if (right == null) throw new ArgumentNullException (nameof (right)); - return SkiaApi.sk_colorspace_equals (left.Handle, right.Handle); + var result = SkiaApi.sk_colorspace_equals (left.Handle, right.Handle); + GC.KeepAlive (left); + GC.KeepAlive (right); + return result; } // CreateSrgb @@ -118,7 +137,8 @@ public SKColorSpaceTransferFn GetNumericalTransferFunction () => public bool GetNumericalTransferFunction (out SKColorSpaceTransferFn fn) { fixed (SKColorSpaceTransferFn* f = &fn) { - return SkiaApi.sk_colorspace_is_numerical_transfer_fn (Handle, f); + var result = SkiaApi.sk_colorspace_is_numerical_transfer_fn (Handle, f); + return result; } } @@ -136,7 +156,8 @@ public SKColorSpaceIccProfile ToProfile () public bool ToColorSpaceXyz (out SKColorSpaceXyz toXyzD50) { fixed (SKColorSpaceXyz* xyz = &toXyzD50) { - return SkiaApi.sk_colorspace_to_xyzd50 (Handle, xyz); + var result = SkiaApi.sk_colorspace_to_xyzd50 (Handle, xyz); + return result; } } @@ -145,11 +166,17 @@ public SKColorSpaceXyz ToColorSpaceXyz () => // To*Gamma - public SKColorSpace ToLinearGamma () => - GetObject (SkiaApi.sk_colorspace_make_linear_gamma (Handle)); + public SKColorSpace ToLinearGamma () + { + var result = GetObject (SkiaApi.sk_colorspace_make_linear_gamma (Handle)); + return result; + } - public SKColorSpace ToSrgbGamma () => - GetObject (SkiaApi.sk_colorspace_make_srgb_gamma (Handle)); + public SKColorSpace ToSrgbGamma () + { + var result = GetObject (SkiaApi.sk_colorspace_make_srgb_gamma (Handle)); + return result; + } // diff --git a/binding/SkiaSharp/SKData.cs b/binding/SkiaSharp/SKData.cs index 1c159842891..503e1fba724 100644 --- a/binding/SkiaSharp/SKData.cs +++ b/binding/SkiaSharp/SKData.cs @@ -28,9 +28,15 @@ internal SKData (IntPtr x, bool owns) protected override void Dispose (bool disposing) => base.Dispose (disposing); - void ISKNonVirtualReferenceCounted.ReferenceNative () => SkiaApi.sk_data_ref (Handle); + void ISKNonVirtualReferenceCounted.ReferenceNative () + { + SkiaApi.sk_data_ref (Handle); + } - void ISKNonVirtualReferenceCounted.UnreferenceNative () => SkiaApi.sk_data_unref (Handle); + void ISKNonVirtualReferenceCounted.UnreferenceNative () + { + SkiaApi.sk_data_unref (Handle); + } public static SKData Empty => LazyInitializer.EnsureInitialized ( @@ -218,7 +224,8 @@ public SKData Subset (ulong offset, ulong length) if (offset > UInt32.MaxValue) throw new ArgumentOutOfRangeException (nameof (offset), "The offset exceeds the size of pointers."); } - return GetObject (SkiaApi.sk_data_new_subset (Handle, (IntPtr)offset, (IntPtr)length)); + var result = GetObject (SkiaApi.sk_data_new_subset (Handle, (IntPtr)offset, (IntPtr)length)); + return result; } // ToArray @@ -226,7 +233,6 @@ public SKData Subset (ulong offset, ulong length) public byte[] ToArray () { var array = AsSpan ().ToArray (); - GC.KeepAlive (this); return array; } @@ -234,9 +240,19 @@ public byte[] ToArray () public bool IsEmpty => Size == 0; - public long Size => (long)SkiaApi.sk_data_get_size (Handle); + public long Size { + get { + var result = (long)SkiaApi.sk_data_get_size (Handle); + return result; + } + } - public IntPtr Data => (IntPtr)SkiaApi.sk_data_get_data (Handle); + public IntPtr Data { + get { + var result = (IntPtr)SkiaApi.sk_data_get_data (Handle); + return result; + } + } public Span Span => new Span ((void*)Data, (int)Size); @@ -272,7 +288,6 @@ public void SaveTo (Stream target) ptr += copyCount; target.Write ((byte[])buffer, 0, copyCount); } - GC.KeepAlive (this); } internal static SKData GetObject (IntPtr handle) => diff --git a/binding/SkiaSharp/SKDocument.cs b/binding/SkiaSharp/SKDocument.cs index eb2d179812a..eeb3a50fb4a 100644 --- a/binding/SkiaSharp/SKDocument.cs +++ b/binding/SkiaSharp/SKDocument.cs @@ -18,20 +18,32 @@ internal SKDocument (IntPtr handle, bool owns) protected override void Dispose (bool disposing) => base.Dispose (disposing); - public void Abort () => + public void Abort () + { SkiaApi.sk_document_abort (Handle); + } - public SKCanvas BeginPage (float width, float height) => - OwnedBy (SKCanvas.GetObject (SkiaApi.sk_document_begin_page (Handle, width, height, null), false), this); + public SKCanvas BeginPage (float width, float height) + { + var result = OwnedBy (SKCanvas.GetObject (SkiaApi.sk_document_begin_page (Handle, width, height, null), false), this); + return result; + } - public SKCanvas BeginPage (float width, float height, SKRect content) => - OwnedBy (SKCanvas.GetObject (SkiaApi.sk_document_begin_page (Handle, width, height, &content), false), this); + public SKCanvas BeginPage (float width, float height, SKRect content) + { + var result = OwnedBy (SKCanvas.GetObject (SkiaApi.sk_document_begin_page (Handle, width, height, &content), false), this); + return result; + } - public void EndPage () => + public void EndPage () + { SkiaApi.sk_document_end_page (Handle); + } - public void Close () => + public void Close () + { SkiaApi.sk_document_close (Handle); + } // CreateXps diff --git a/binding/SkiaSharp/SKDrawable.cs b/binding/SkiaSharp/SKDrawable.cs index c23cf0a04f8..ef22260e616 100644 --- a/binding/SkiaSharp/SKDrawable.cs +++ b/binding/SkiaSharp/SKDrawable.cs @@ -54,7 +54,12 @@ protected override void DisposeNative () SkiaApi.sk_drawable_unref (Handle); } - public uint GenerationId => SkiaApi.sk_drawable_get_generation_id (Handle); + public uint GenerationId { + get { + var result = SkiaApi.sk_drawable_get_generation_id (Handle); + return result; + } + } public SKRect Bounds { get { @@ -64,13 +69,18 @@ public SKRect Bounds { } } - public int ApproximateBytesUsed => - (int)SkiaApi.sk_drawable_approximate_bytes_used (Handle); + public int ApproximateBytesUsed { + get { + var result = (int)SkiaApi.sk_drawable_approximate_bytes_used (Handle); + return result; + } + } public void Draw (SKCanvas canvas, in SKMatrix matrix) { fixed (SKMatrix* m = &matrix) SkiaApi.sk_drawable_draw (Handle, canvas.Handle, m); + GC.KeepAlive (canvas); } public void Draw (SKCanvas canvas, float x, float y) @@ -80,11 +90,16 @@ public void Draw (SKCanvas canvas, float x, float y) } // do not unref as this is a plain pointer return, not a reference counted pointer - public SKPicture Snapshot () => - SKPicture.GetObject (SkiaApi.sk_drawable_new_picture_snapshot (Handle), unrefExisting: false); + public SKPicture Snapshot () + { + var result = SKPicture.GetObject (SkiaApi.sk_drawable_new_picture_snapshot (Handle), unrefExisting: false); + return result; + } - public void NotifyDrawingChanged () => + public void NotifyDrawingChanged () + { SkiaApi.sk_drawable_notify_drawing_changed (Handle); + } protected internal virtual void OnDraw (SKCanvas canvas) { diff --git a/binding/SkiaSharp/SKFont.cs b/binding/SkiaSharp/SKFont.cs index 71beb79b96f..c47ffcc3a1d 100644 --- a/binding/SkiaSharp/SKFont.cs +++ b/binding/SkiaSharp/SKFont.cs @@ -25,77 +25,145 @@ public SKFont () public SKFont (SKTypeface typeface, float size = DefaultSize, float scaleX = DefaultScaleX, float skewX = DefaultSkewX) : this (SkiaApi.sk_font_new_with_values (typeface?.Handle ?? IntPtr.Zero, size, scaleX, skewX), true) { + GC.KeepAlive (typeface); if (Handle == IntPtr.Zero) throw new InvalidOperationException ("Unable to create a new SKFont instance."); } - protected override void DisposeNative () => + protected override void DisposeNative () + { SkiaApi.sk_font_delete (Handle); + } public bool ForceAutoHinting { - get => SkiaApi.sk_font_is_force_auto_hinting (Handle); - set => SkiaApi.sk_font_set_force_auto_hinting (Handle, value); + get { + var r = SkiaApi.sk_font_is_force_auto_hinting (Handle); + return r; + } + set { + SkiaApi.sk_font_set_force_auto_hinting (Handle, value); + } } public bool EmbeddedBitmaps { - get => SkiaApi.sk_font_is_embedded_bitmaps (Handle); - set => SkiaApi.sk_font_set_embedded_bitmaps (Handle, value); + get { + var r = SkiaApi.sk_font_is_embedded_bitmaps (Handle); + return r; + } + set { + SkiaApi.sk_font_set_embedded_bitmaps (Handle, value); + } } public bool Subpixel { - get => SkiaApi.sk_font_is_subpixel (Handle); - set => SkiaApi.sk_font_set_subpixel (Handle, value); + get { + var r = SkiaApi.sk_font_is_subpixel (Handle); + return r; + } + set { + SkiaApi.sk_font_set_subpixel (Handle, value); + } } public bool LinearMetrics { - get => SkiaApi.sk_font_is_linear_metrics (Handle); - set => SkiaApi.sk_font_set_linear_metrics (Handle, value); + get { + var r = SkiaApi.sk_font_is_linear_metrics (Handle); + return r; + } + set { + SkiaApi.sk_font_set_linear_metrics (Handle, value); + } } public bool Embolden { - get => SkiaApi.sk_font_is_embolden (Handle); - set => SkiaApi.sk_font_set_embolden (Handle, value); + get { + var r = SkiaApi.sk_font_is_embolden (Handle); + return r; + } + set { + SkiaApi.sk_font_set_embolden (Handle, value); + } } public bool BaselineSnap { - get => SkiaApi.sk_font_is_baseline_snap (Handle); - set => SkiaApi.sk_font_set_baseline_snap (Handle, value); + get { + var r = SkiaApi.sk_font_is_baseline_snap (Handle); + return r; + } + set { + SkiaApi.sk_font_set_baseline_snap (Handle, value); + } } public SKFontEdging Edging { - get => SkiaApi.sk_font_get_edging (Handle); - set => SkiaApi.sk_font_set_edging (Handle, value); + get { + var r = SkiaApi.sk_font_get_edging (Handle); + return r; + } + set { + SkiaApi.sk_font_set_edging (Handle, value); + } } public SKFontHinting Hinting { - get => SkiaApi.sk_font_get_hinting (Handle); - set => SkiaApi.sk_font_set_hinting (Handle, value); + get { + var r = SkiaApi.sk_font_get_hinting (Handle); + return r; + } + set { + SkiaApi.sk_font_set_hinting (Handle, value); + } } public SKTypeface Typeface { - get => SKTypeface.GetObject (SkiaApi.sk_font_get_typeface (Handle)); - set => SkiaApi.sk_font_set_typeface (Handle, value == null ? IntPtr.Zero : value.Handle); + get { + var r = SKTypeface.GetObject (SkiaApi.sk_font_get_typeface (Handle)); + return r; + } + set { + SkiaApi.sk_font_set_typeface (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } public float Size { - get => SkiaApi.sk_font_get_size (Handle); - set => SkiaApi.sk_font_set_size (Handle, value); + get { + var r = SkiaApi.sk_font_get_size (Handle); + return r; + } + set { + SkiaApi.sk_font_set_size (Handle, value); + } } public float ScaleX { - get => SkiaApi.sk_font_get_scale_x (Handle); - set => SkiaApi.sk_font_set_scale_x (Handle, value); + get { + var r = SkiaApi.sk_font_get_scale_x (Handle); + return r; + } + set { + SkiaApi.sk_font_set_scale_x (Handle, value); + } } public float SkewX { - get => SkiaApi.sk_font_get_skew_x (Handle); - set => SkiaApi.sk_font_set_skew_x (Handle, value); + get { + var r = SkiaApi.sk_font_get_skew_x (Handle); + return r; + } + set { + SkiaApi.sk_font_set_skew_x (Handle, value); + } } // FontSpacing - public float Spacing => - SkiaApi.sk_font_get_metrics (Handle, null); + public float Spacing { + get { + var r = SkiaApi.sk_font_get_metrics (Handle, null); + return r; + } + } // FontMetrics @@ -109,14 +177,18 @@ public SKFontMetrics Metrics { public float GetFontMetrics (out SKFontMetrics metrics) { fixed (SKFontMetrics* m = &metrics) { - return SkiaApi.sk_font_get_metrics (Handle, m); + var r = SkiaApi.sk_font_get_metrics (Handle, m); + return r; } } // GetGlyph - public ushort GetGlyph (int codepoint) => - SkiaApi.sk_font_unichar_to_glyph (Handle, codepoint); + public ushort GetGlyph (int codepoint) + { + var r = SkiaApi.sk_font_unichar_to_glyph (Handle, codepoint); + return r; + } // GetGlyphs @@ -259,7 +331,8 @@ internal int CountGlyphs (void* text, int length, SKTextEncoding encoding) if (!ValidateTextArgs (text, length, encoding)) return 0; - return SkiaApi.sk_font_text_to_glyphs (Handle, text, (IntPtr)length, encoding, null, 0); + var r = SkiaApi.sk_font_text_to_glyphs (Handle, text, (IntPtr)length, encoding, null, 0); + return r; } // MeasureText (text) @@ -317,6 +390,7 @@ internal float MeasureText (void* text, int length, SKTextEncoding encoding, SKR float measuredWidth; SkiaApi.sk_font_measure_text_no_return (Handle, text, (IntPtr)length, encoding, bounds, paint?.Handle ?? IntPtr.Zero, &measuredWidth); + GC.KeepAlive (paint); return measuredWidth; } @@ -383,7 +457,9 @@ internal int BreakText (void* text, int length, SKTextEncoding encoding, float m if (!ValidateTextArgs (text, length, encoding)) return 0; - return (int)SkiaApi.sk_font_break_text (Handle, text, (IntPtr)length, encoding, maxWidth, measuredWidth, paint?.Handle ?? IntPtr.Zero); + var result = (int)SkiaApi.sk_font_break_text (Handle, text, (IntPtr)length, encoding, maxWidth, measuredWidth, paint?.Handle ?? IntPtr.Zero); + GC.KeepAlive (paint); + return result; } // GetGlyphPositions (text) @@ -707,6 +783,7 @@ public void GetGlyphWidths (ReadOnlySpan glyphs, Span widths, Spa var b = bounds.Length > 0 ? bp : null; SkiaApi.sk_font_get_widths_bounds (Handle, gp, glyphs.Length, w, b, paint?.Handle ?? IntPtr.Zero); } + GC.KeepAlive (paint); } // GetGlyphPath diff --git a/binding/SkiaSharp/SKFontManager.cs b/binding/SkiaSharp/SKFontManager.cs index 94c4a9419d2..08bbfce1cde 100644 --- a/binding/SkiaSharp/SKFontManager.cs +++ b/binding/SkiaSharp/SKFontManager.cs @@ -28,7 +28,12 @@ protected override void Dispose (bool disposing) => ref defaultManager, ref defaultManagerInitialized, ref defaultManagerLock, () => GetDisposeProtectedObject (SkiaApi.sk_fontmgr_create_default ())); - public int FontFamilyCount => SkiaApi.sk_fontmgr_count_families (Handle); + public int FontFamilyCount { + get { + var r = SkiaApi.sk_fontmgr_count_families (Handle); + return r; + } + } public IEnumerable FontFamilies { get { @@ -50,14 +55,16 @@ public string GetFamilyName (int index) public SKFontStyleSet GetFontStyles (int index) { - return SKFontStyleSet.GetObject (SkiaApi.sk_fontmgr_create_styleset (Handle, index)); + var styleSet = SKFontStyleSet.GetObject (SkiaApi.sk_fontmgr_create_styleset (Handle, index)); + return styleSet; } public SKFontStyleSet GetFontStyles (string familyName) { var familyNameUtf8ByteList = StringUtilities.GetEncodedText (familyName, SKTextEncoding.Utf8, addNull: true); fixed (byte* familyNamePointer = familyNameUtf8ByteList) { - return SKFontStyleSet.GetObject (SkiaApi.sk_fontmgr_match_family (Handle, new IntPtr (familyNamePointer))); + var styleSet = SKFontStyleSet.GetObject (SkiaApi.sk_fontmgr_match_family (Handle, new IntPtr (familyNamePointer))); + return styleSet; } } @@ -70,7 +77,9 @@ public SKTypeface MatchFamily (string familyName, SKFontStyle style) throw new ArgumentNullException (nameof (style)); var familyNameUtf8ByteList = StringUtilities.GetEncodedText (familyName, SKTextEncoding.Utf8, addNull: true); fixed (byte* familyNamePointer = familyNameUtf8ByteList) { - return SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontmgr_match_family_style (Handle, new IntPtr (familyNamePointer), style.Handle)); + var typeface = SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontmgr_match_family_style (Handle, new IntPtr (familyNamePointer), style.Handle)); + GC.KeepAlive (style); + return typeface; } } @@ -81,7 +90,8 @@ public SKTypeface CreateTypeface (string path, int index = 0) var utf8path = StringUtilities.GetEncodedText (path, SKTextEncoding.Utf8, true); fixed (byte* u = utf8path) { - return SKTypeface.GetObject (SkiaApi.sk_fontmgr_create_from_file (Handle, u, index)); + var typeface = SKTypeface.GetObject (SkiaApi.sk_fontmgr_create_from_file (Handle, u, index)); + return typeface; } } @@ -113,7 +123,9 @@ public SKTypeface CreateTypeface (SKData data, int index = 0) if (data == null) throw new ArgumentNullException (nameof (data)); - return SKTypeface.GetObject (SkiaApi.sk_fontmgr_create_from_data (Handle, data.Handle, index)); + var typeface = SKTypeface.GetObject (SkiaApi.sk_fontmgr_create_from_data (Handle, data.Handle, index)); + GC.KeepAlive (data); + return typeface; } public SKTypeface MatchCharacter (char character) @@ -172,7 +184,9 @@ public SKTypeface MatchCharacter (string familyName, SKFontStyle style, string[] var familyNameUtf8ByteList = StringUtilities.GetEncodedText (familyName, SKTextEncoding.Utf8, addNull: true); fixed (byte* familyNamePointer = familyNameUtf8ByteList) { - return SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontmgr_match_family_style_character (Handle, new IntPtr (familyNamePointer), style.Handle, bcp47, bcp47?.Length ?? 0, character)); + var typeface = SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontmgr_match_family_style_character (Handle, new IntPtr (familyNamePointer), style.Handle, bcp47, bcp47?.Length ?? 0, character)); + GC.KeepAlive (style); + return typeface; } } diff --git a/binding/SkiaSharp/SKFontStyleSet.cs b/binding/SkiaSharp/SKFontStyleSet.cs index 1983d991cbd..409e5c7a469 100644 --- a/binding/SkiaSharp/SKFontStyleSet.cs +++ b/binding/SkiaSharp/SKFontStyleSet.cs @@ -21,7 +21,12 @@ public SKFontStyleSet () protected override void Dispose (bool disposing) => base.Dispose (disposing); - public int Count => SkiaApi.sk_fontstyleset_get_count (Handle); + public int Count { + get { + var r = SkiaApi.sk_fontstyleset_get_count (Handle); + return r; + } + } public SKFontStyle this[int index] => GetStyle (index); @@ -29,7 +34,6 @@ public string GetStyleName (int index) { using var str = new SKString (); SkiaApi.sk_fontstyleset_get_style (Handle, index, IntPtr.Zero, str.Handle); - GC.KeepAlive (this); return (string)str; } @@ -39,7 +43,6 @@ public SKTypeface CreateTypeface (int index) throw new ArgumentOutOfRangeException ($"Index was out of range. Must be non-negative and less than the size of the set.", nameof (index)); var tf = SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontstyleset_create_typeface (Handle, index)); - GC.KeepAlive (this); return tf; } @@ -49,7 +52,7 @@ public SKTypeface CreateTypeface (SKFontStyle style) throw new ArgumentNullException (nameof (style)); var tf = SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontstyleset_match_style (Handle, style.Handle)); - GC.KeepAlive (this); + GC.KeepAlive (style); return tf; } diff --git a/binding/SkiaSharp/SKGraphics.cs b/binding/SkiaSharp/SKGraphics.cs index 5399d5c958a..b36c0d64e40 100644 --- a/binding/SkiaSharp/SKGraphics.cs +++ b/binding/SkiaSharp/SKGraphics.cs @@ -59,7 +59,10 @@ public static long SetResourceCacheSingleAllocationByteLimit (long bytes) => // dump - public static void DumpMemoryStatistics (SKTraceMemoryDump dump) => + public static void DumpMemoryStatistics (SKTraceMemoryDump dump) + { SkiaApi.sk_graphics_dump_memory_statistics (dump?.Handle ?? throw new ArgumentNullException (nameof (dump))); + GC.KeepAlive (dump); + } } } diff --git a/binding/SkiaSharp/SKImage.cs b/binding/SkiaSharp/SKImage.cs index b7d2e38fe5c..3ba6ca410df 100644 --- a/binding/SkiaSharp/SKImage.cs +++ b/binding/SkiaSharp/SKImage.cs @@ -88,7 +88,9 @@ public static SKImage FromPixelCopy (SKPixmap pixmap) { if (pixmap == null) throw new ArgumentNullException (nameof (pixmap)); - return GetObject (SkiaApi.sk_image_new_raster_copy_with_pixmap (pixmap.Handle)); + var image = GetObject (SkiaApi.sk_image_new_raster_copy_with_pixmap (pixmap.Handle)); + GC.KeepAlive (pixmap); + return image; } public static SKImage FromPixelCopy (SKImageInfo info, ReadOnlySpan pixels) => @@ -111,7 +113,9 @@ public static SKImage FromPixels (SKImageInfo info, SKData data, int rowBytes) if (data == null) throw new ArgumentNullException (nameof (data)); var cinfo = SKImageInfoNative.FromManaged (ref info); - return GetObject (SkiaApi.sk_image_new_raster_data (&cinfo, data.Handle, (IntPtr)rowBytes)); + var image = GetObject (SkiaApi.sk_image_new_raster_data (&cinfo, data.Handle, (IntPtr)rowBytes)); + GC.KeepAlive (data); + return image; } public static SKImage FromPixels (SKImageInfo info, IntPtr pixels) @@ -148,7 +152,9 @@ public static SKImage FromPixels (SKPixmap pixmap, SKImageRasterReleaseDelegate : releaseProc; DelegateProxies.Create (del, out _, out var ctx); var proxy = del is not null ? DelegateProxies.SKImageRasterReleaseProxy : null; - return GetObject (SkiaApi.sk_image_new_raster (pixmap.Handle, proxy, (void*)ctx)); + var image = GetObject (SkiaApi.sk_image_new_raster (pixmap.Handle, proxy, (void*)ctx)); + GC.KeepAlive (pixmap); + return image; } // create a new image from encoded data @@ -167,6 +173,7 @@ public static SKImage FromEncodedData (SKData data) throw new ArgumentNullException (nameof (data)); var handle = SkiaApi.sk_image_new_from_encoded (data.Handle); + GC.KeepAlive (data); return GetObject (handle); } @@ -288,7 +295,11 @@ public static SKImage FromTexture (GRRecordingContext context, GRBackendTexture : releaseProc; DelegateProxies.Create (del, out _, out var ctx); var proxy = del is not null ? DelegateProxies.SKImageTextureReleaseProxy : null; - return GetObject (SkiaApi.sk_image_new_from_texture (context.Handle, texture.Handle, origin, colorType.ToNative (), alpha, cs, proxy, (void*)ctx)); + var image = GetObject (SkiaApi.sk_image_new_from_texture (context.Handle, texture.Handle, origin, colorType.ToNative (), alpha, cs, proxy, (void*)ctx)); + GC.KeepAlive (context); + GC.KeepAlive (texture); + GC.KeepAlive (colorspace); + return image; } public static SKImage FromAdoptedTexture (GRContext context, GRBackendTexture texture, SKColorType colorType) => @@ -320,7 +331,11 @@ public static SKImage FromAdoptedTexture (GRRecordingContext context, GRBackendT throw new ArgumentNullException (nameof (texture)); var cs = colorspace == null ? IntPtr.Zero : colorspace.Handle; - return GetObject (SkiaApi.sk_image_new_from_adopted_texture (context.Handle, texture.Handle, origin, colorType.ToNative (), alpha, cs)); + var image = GetObject (SkiaApi.sk_image_new_from_adopted_texture (context.Handle, texture.Handle, origin, colorType.ToNative (), alpha, cs)); + GC.KeepAlive (context); + GC.KeepAlive (texture); + GC.KeepAlive (colorspace); + return image; } // create a new image from a picture @@ -345,7 +360,12 @@ private static SKImage FromPicture (SKPicture picture, SKSizeI dimensions, SKMat var p = paint?.Handle ?? IntPtr.Zero; var cs = colorspace?.Handle ?? IntPtr.Zero; var prps = props?.Handle ?? IntPtr.Zero; - return GetObject (SkiaApi.sk_image_new_from_picture (picture.Handle, &dimensions, matrix, p, useFloatingPointBitDepth, cs, prps)); + var image = GetObject (SkiaApi.sk_image_new_from_picture (picture.Handle, &dimensions, matrix, p, useFloatingPointBitDepth, cs, prps)); + GC.KeepAlive (picture); + GC.KeepAlive (paint); + GC.KeepAlive (colorspace); + GC.KeepAlive (props); + return image; } public SKData Encode () @@ -368,29 +388,61 @@ public SKData Encode (SKEncodedImageFormat format, int quality) } } - public int Width => - SkiaApi.sk_image_get_width (Handle); + public int Width { + get { + var result = SkiaApi.sk_image_get_width (Handle); + return result; + } + } - public int Height => - SkiaApi.sk_image_get_height (Handle); + public int Height { + get { + var result = SkiaApi.sk_image_get_height (Handle); + return result; + } + } - public uint UniqueId => - SkiaApi.sk_image_get_unique_id (Handle); + public uint UniqueId { + get { + var result = SkiaApi.sk_image_get_unique_id (Handle); + return result; + } + } - public SKAlphaType AlphaType => - SkiaApi.sk_image_get_alpha_type (Handle); + public SKAlphaType AlphaType { + get { + var result = SkiaApi.sk_image_get_alpha_type (Handle); + return result; + } + } - public SKColorType ColorType => - SkiaApi.sk_image_get_color_type (Handle).FromNative (); + public SKColorType ColorType { + get { + var result = SkiaApi.sk_image_get_color_type (Handle).FromNative (); + return result; + } + } - public SKColorSpace ColorSpace => - SKColorSpace.GetObject (SkiaApi.sk_image_get_colorspace (Handle)); + public SKColorSpace ColorSpace { + get { + var result = SKColorSpace.GetObject (SkiaApi.sk_image_get_colorspace (Handle)); + return result; + } + } - public bool IsAlphaOnly => - SkiaApi.sk_image_is_alpha_only (Handle); + public bool IsAlphaOnly { + get { + var result = SkiaApi.sk_image_is_alpha_only (Handle); + return result; + } + } - public SKData EncodedData => - SKData.GetObject (SkiaApi.sk_image_ref_encoded (Handle)); + public SKData EncodedData { + get { + var result = SKData.GetObject (SkiaApi.sk_image_ref_encoded (Handle)); + return result; + } + } public SKImageInfo Info => new SKImageInfo (Width, Height, ColorType, AlphaType, ColorSpace); @@ -420,8 +472,11 @@ public SKShader ToShader (SKShaderTileMode tileX, SKShaderTileMode tileY, SKSamp public SKShader ToShader (SKShaderTileMode tileX, SKShaderTileMode tileY, SKFilterQuality quality, SKMatrix localMatrix) => ToShader (tileX, tileY, quality.ToSamplingOptions(), &localMatrix); - private SKShader ToShader (SKShaderTileMode tileX, SKShaderTileMode tileY, SKSamplingOptions sampling, SKMatrix* localMatrix) => - SKShader.GetObject (SkiaApi.sk_image_make_shader (Handle, tileX, tileY, &sampling, localMatrix)); + private SKShader ToShader (SKShaderTileMode tileX, SKShaderTileMode tileY, SKSamplingOptions sampling, SKMatrix* localMatrix) + { + var result = SKShader.GetObject (SkiaApi.sk_image_make_shader (Handle, tileX, tileY, &sampling, localMatrix)); + return result; + } // ToRawShader @@ -440,8 +495,11 @@ public SKShader ToRawShader (SKShaderTileMode tileX, SKShaderTileMode tileY, SKS public SKShader ToRawShader (SKShaderTileMode tileX, SKShaderTileMode tileY, SKSamplingOptions sampling, SKMatrix localMatrix) => ToRawShader (tileX, tileY, sampling, &localMatrix); - private SKShader ToRawShader (SKShaderTileMode tileX, SKShaderTileMode tileY, SKSamplingOptions sampling, SKMatrix* localMatrix) => - SKShader.GetObject (SkiaApi.sk_image_make_raw_shader (Handle, tileX, tileY, &sampling, localMatrix)); + private SKShader ToRawShader (SKShaderTileMode tileX, SKShaderTileMode tileY, SKSamplingOptions sampling, SKMatrix* localMatrix) + { + var result = SKShader.GetObject (SkiaApi.sk_image_make_raw_shader (Handle, tileX, tileY, &sampling, localMatrix)); + return result; + } // PeekPixels @@ -451,6 +509,7 @@ public bool PeekPixels (SKPixmap pixmap) throw new ArgumentNullException (nameof (pixmap)); var result = SkiaApi.sk_image_peek_pixels (Handle, pixmap.Handle); + GC.KeepAlive (pixmap); if (result) pixmap.pixelSource = this; return result; @@ -466,17 +525,29 @@ public SKPixmap PeekPixels () return pixmap; } - public bool IsTextureBacked => - SkiaApi.sk_image_is_texture_backed (Handle); + public bool IsTextureBacked { + get { + var result = SkiaApi.sk_image_is_texture_backed (Handle); + return result; + } + } - public bool IsLazyGenerated => - SkiaApi.sk_image_is_lazy_generated (Handle); + public bool IsLazyGenerated { + get { + var result = SkiaApi.sk_image_is_lazy_generated (Handle); + return result; + } + } public bool IsValid (GRContext context) => IsValid ((GRRecordingContext)context); - public bool IsValid (GRRecordingContext context) => - SkiaApi.sk_image_is_valid (Handle, context?.Handle ?? IntPtr.Zero); + public bool IsValid (GRRecordingContext context) + { + var result = SkiaApi.sk_image_is_valid (Handle, context?.Handle ?? IntPtr.Zero); + GC.KeepAlive (context); + return result; + } // ReadPixels @@ -493,7 +564,6 @@ public bool ReadPixels (SKImageInfo dstInfo, IntPtr dstPixels, int dstRowBytes, { var cinfo = SKImageInfoNative.FromManaged (ref dstInfo); var result = SkiaApi.sk_image_read_pixels (Handle, &cinfo, (void*)dstPixels, (IntPtr)dstRowBytes, srcX, srcY, cachingHint); - GC.KeepAlive (this); return result; } @@ -509,7 +579,7 @@ public bool ReadPixels (SKPixmap pixmap, int srcX, int srcY, SKImageCachingHint throw new ArgumentNullException (nameof (pixmap)); var result = SkiaApi.sk_image_read_pixels_into_pixmap (Handle, pixmap.Handle, srcX, srcY, cachingHint); - GC.KeepAlive (this); + GC.KeepAlive (pixmap); return result; } @@ -532,19 +602,24 @@ public bool ScalePixels (SKPixmap dst, SKSamplingOptions sampling, SKImageCachin { if (dst == null) throw new ArgumentNullException (nameof (dst)); - return SkiaApi.sk_image_scale_pixels (Handle, dst.Handle, &sampling, cachingHint); + var result = SkiaApi.sk_image_scale_pixels (Handle, dst.Handle, &sampling, cachingHint); + GC.KeepAlive (dst); + return result; } // Subset public SKImage Subset (SKRectI subset) { - return GetObject (SkiaApi.sk_image_make_subset_raster (Handle, &subset)); + var image = GetObject (SkiaApi.sk_image_make_subset_raster (Handle, &subset)); + return image; } public SKImage Subset (GRRecordingContext context, SKRectI subset) { - return GetObject (SkiaApi.sk_image_make_subset (Handle, context?.Handle ?? IntPtr.Zero, &subset)); + var image = GetObject (SkiaApi.sk_image_make_subset (Handle, context?.Handle ?? IntPtr.Zero, &subset)); + GC.KeepAlive (context); + return image; } // ToRasterImage @@ -552,10 +627,13 @@ public SKImage Subset (GRRecordingContext context, SKRectI subset) public SKImage ToRasterImage () => ToRasterImage (false); - public SKImage ToRasterImage (bool ensurePixelData) => - ensurePixelData + public SKImage ToRasterImage (bool ensurePixelData) + { + var image = ensurePixelData ? GetObject (SkiaApi.sk_image_make_raster_image (Handle)) : GetObject (SkiaApi.sk_image_make_non_texture_image (Handle)); + return image; + } // ToTextureImage @@ -570,7 +648,9 @@ public SKImage ToTextureImage (GRContext context, bool mipmapped, bool budgeted) if (context == null) throw new ArgumentNullException (nameof (context)); - return GetObject (SkiaApi.sk_image_make_texture_image (Handle, context.Handle, mipmapped, budgeted)); + var image = GetObject (SkiaApi.sk_image_make_texture_image (Handle, context.Handle, mipmapped, budgeted)); + GC.KeepAlive (context); + return image; } // ApplyImageFilter @@ -589,7 +669,9 @@ public SKImage ApplyImageFilter (SKImageFilter filter, SKRectI subset, SKRectI c fixed (SKRectI* os = &outSubset) fixed (SKPointI* oo = &outOffset) { - return GetObject (SkiaApi.sk_image_make_with_filter_raster (Handle, filter.Handle, &subset, &clipBounds, os, oo)); + var image = GetObject (SkiaApi.sk_image_make_with_filter_raster (Handle, filter.Handle, &subset, &clipBounds, os, oo)); + GC.KeepAlive (filter); + return image; } } @@ -603,7 +685,10 @@ public SKImage ApplyImageFilter (GRRecordingContext context, SKImageFilter filte fixed (SKRectI* os = &outSubset) fixed (SKPointI* oo = &outOffset) { - return GetObject (SkiaApi.sk_image_make_with_filter (Handle, context?.Handle ?? IntPtr.Zero, filter.Handle, &subset, &clipBounds, os, oo)); + var image = GetObject (SkiaApi.sk_image_make_with_filter (Handle, context?.Handle ?? IntPtr.Zero, filter.Handle, &subset, &clipBounds, os, oo)); + GC.KeepAlive (context); + GC.KeepAlive (filter); + return image; } } diff --git a/binding/SkiaSharp/SKImageFilter.cs b/binding/SkiaSharp/SKImageFilter.cs index 45eea572e36..6bf526cade5 100644 --- a/binding/SkiaSharp/SKImageFilter.cs +++ b/binding/SkiaSharp/SKImageFilter.cs @@ -30,8 +30,11 @@ public static SKImageFilter CreateMatrix (in SKMatrix matrix, SKSamplingOptions public static SKImageFilter CreateMatrix (in SKMatrix matrix, SKSamplingOptions sampling, SKImageFilter? input) { - fixed (SKMatrix* m = &matrix) - return GetObject (SkiaApi.sk_imagefilter_new_matrix_transform (m, &sampling, input?.Handle ?? IntPtr.Zero)); + fixed (SKMatrix* m = &matrix) { + var filter = GetObject (SkiaApi.sk_imagefilter_new_matrix_transform (m, &sampling, input?.Handle ?? IntPtr.Zero)); + GC.KeepAlive (input); + return filter; + } } // CreateBlur @@ -54,8 +57,12 @@ public static SKImageFilter CreateBlur (float sigmaX, float sigmaY, SKShaderTile public static SKImageFilter CreateBlur (float sigmaX, float sigmaY, SKShaderTileMode tileMode, SKImageFilter? input, SKRect cropRect) => CreateBlur (sigmaX, sigmaY, tileMode, input, &cropRect); - private static SKImageFilter CreateBlur (float sigmaX, float sigmaY, SKShaderTileMode tileMode, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_blur (sigmaX, sigmaY, tileMode, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateBlur (float sigmaX, float sigmaY, SKShaderTileMode tileMode, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_blur (sigmaX, sigmaY, tileMode, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreateColorFilter @@ -71,7 +78,10 @@ public static SKImageFilter CreateColorFilter (SKColorFilter cf, SKImageFilter? private static SKImageFilter CreateColorFilter (SKColorFilter cf, SKImageFilter? input, SKRect* cropRect) { _ = cf ?? throw new ArgumentNullException (nameof (cf)); - return GetObject (SkiaApi.sk_imagefilter_new_color_filter (cf.Handle, input?.Handle ?? IntPtr.Zero, cropRect)); + var filter = GetObject (SkiaApi.sk_imagefilter_new_color_filter (cf.Handle, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (cf); + GC.KeepAlive (input); + return filter; } // CreateCompose @@ -80,7 +90,10 @@ public static SKImageFilter CreateCompose (SKImageFilter outer, SKImageFilter in { _ = outer ?? throw new ArgumentNullException (nameof (outer)); _ = inner ?? throw new ArgumentNullException (nameof (inner)); - return GetObject (SkiaApi.sk_imagefilter_new_compose (outer.Handle, inner.Handle)); + var filter = GetObject (SkiaApi.sk_imagefilter_new_compose (outer.Handle, inner.Handle)); + GC.KeepAlive (outer); + GC.KeepAlive (inner); + return filter; } // CreateDisplacementMapEffect @@ -97,7 +110,10 @@ public static SKImageFilter CreateDisplacementMapEffect (SKColorChannel xChannel private static SKImageFilter CreateDisplacementMapEffect (SKColorChannel xChannelSelector, SKColorChannel yChannelSelector, float scale, SKImageFilter displacement, SKImageFilter? input, SKRect* cropRect) { _ = displacement ?? throw new ArgumentNullException (nameof (displacement)); - return GetObject (SkiaApi.sk_imagefilter_new_displacement_map_effect (xChannelSelector, yChannelSelector, scale, displacement.Handle, input?.Handle ?? IntPtr.Zero, cropRect)); + var filter = GetObject (SkiaApi.sk_imagefilter_new_displacement_map_effect (xChannelSelector, yChannelSelector, scale, displacement.Handle, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (displacement); + GC.KeepAlive (input); + return filter; } // CreateDropShadow @@ -111,8 +127,12 @@ public static SKImageFilter CreateDropShadow (float dx, float dy, float sigmaX, public static SKImageFilter CreateDropShadow (float dx, float dy, float sigmaX, float sigmaY, SKColor color, SKImageFilter? input, SKRect cropRect) => CreateDropShadow (dx, dy, sigmaX, sigmaY, color, input, &cropRect); - private static SKImageFilter CreateDropShadow (float dx, float dy, float sigmaX, float sigmaY, SKColor color, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_drop_shadow (dx, dy, sigmaX, sigmaY, (uint)color, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateDropShadow (float dx, float dy, float sigmaX, float sigmaY, SKColor color, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_drop_shadow (dx, dy, sigmaX, sigmaY, (uint)color, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreateDropShadowOnly @@ -125,8 +145,12 @@ public static SKImageFilter CreateDropShadowOnly (float dx, float dy, float sigm public static SKImageFilter CreateDropShadowOnly (float dx, float dy, float sigmaX, float sigmaY, SKColor color, SKImageFilter? input, SKRect cropRect) => CreateDropShadowOnly (dx, dy, sigmaX, sigmaY, color, input, &cropRect); - private static SKImageFilter CreateDropShadowOnly (float dx, float dy, float sigmaX, float sigmaY, SKColor color, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_drop_shadow_only (dx, dy, sigmaX, sigmaY, (uint)color, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateDropShadowOnly (float dx, float dy, float sigmaX, float sigmaY, SKColor color, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_drop_shadow_only (dx, dy, sigmaX, sigmaY, (uint)color, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreateDistantLitDiffuse @@ -139,8 +163,12 @@ public static SKImageFilter CreateDistantLitDiffuse (SKPoint3 direction, SKColor public static SKImageFilter CreateDistantLitDiffuse (SKPoint3 direction, SKColor lightColor, float surfaceScale, float kd, SKImageFilter? input, SKRect cropRect) => CreateDistantLitDiffuse (direction, lightColor, surfaceScale, kd, input, &cropRect); - private static SKImageFilter CreateDistantLitDiffuse (SKPoint3 direction, SKColor lightColor, float surfaceScale, float kd, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_distant_lit_diffuse (&direction, (uint)lightColor, surfaceScale, kd, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateDistantLitDiffuse (SKPoint3 direction, SKColor lightColor, float surfaceScale, float kd, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_distant_lit_diffuse (&direction, (uint)lightColor, surfaceScale, kd, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreatePointLitDiffuse @@ -153,8 +181,12 @@ public static SKImageFilter CreatePointLitDiffuse (SKPoint3 location, SKColor li public static SKImageFilter CreatePointLitDiffuse (SKPoint3 location, SKColor lightColor, float surfaceScale, float kd, SKImageFilter? input, SKRect cropRect) => CreatePointLitDiffuse (location, lightColor, surfaceScale, kd, input, &cropRect); - private static SKImageFilter CreatePointLitDiffuse (SKPoint3 location, SKColor lightColor, float surfaceScale, float kd, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_point_lit_diffuse (&location, (uint)lightColor, surfaceScale, kd, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreatePointLitDiffuse (SKPoint3 location, SKColor lightColor, float surfaceScale, float kd, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_point_lit_diffuse (&location, (uint)lightColor, surfaceScale, kd, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreateSpotLitDiffuse @@ -167,8 +199,12 @@ public static SKImageFilter CreateSpotLitDiffuse (SKPoint3 location, SKPoint3 ta public static SKImageFilter CreateSpotLitDiffuse (SKPoint3 location, SKPoint3 target, float specularExponent, float cutoffAngle, SKColor lightColor, float surfaceScale, float kd, SKImageFilter? input, SKRect cropRect) => CreateSpotLitDiffuse (location, target, specularExponent, cutoffAngle, lightColor, surfaceScale, kd, input, &cropRect); - private static SKImageFilter CreateSpotLitDiffuse (SKPoint3 location, SKPoint3 target, float specularExponent, float cutoffAngle, SKColor lightColor, float surfaceScale, float kd, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_spot_lit_diffuse (&location, &target, specularExponent, cutoffAngle, (uint)lightColor, surfaceScale, kd, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateSpotLitDiffuse (SKPoint3 location, SKPoint3 target, float specularExponent, float cutoffAngle, SKColor lightColor, float surfaceScale, float kd, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_spot_lit_diffuse (&location, &target, specularExponent, cutoffAngle, (uint)lightColor, surfaceScale, kd, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreateDistantLitSpecular @@ -181,8 +217,12 @@ public static SKImageFilter CreateDistantLitSpecular (SKPoint3 direction, SKColo public static SKImageFilter CreateDistantLitSpecular (SKPoint3 direction, SKColor lightColor, float surfaceScale, float ks, float shininess, SKImageFilter? input, SKRect cropRect) => CreateDistantLitSpecular (direction, lightColor, surfaceScale, ks, shininess, input, &cropRect); - private static SKImageFilter CreateDistantLitSpecular (SKPoint3 direction, SKColor lightColor, float surfaceScale, float ks, float shininess, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_distant_lit_specular (&direction, (uint)lightColor, surfaceScale, ks, shininess, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateDistantLitSpecular (SKPoint3 direction, SKColor lightColor, float surfaceScale, float ks, float shininess, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_distant_lit_specular (&direction, (uint)lightColor, surfaceScale, ks, shininess, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreatePointLitSpecular @@ -195,8 +235,12 @@ public static SKImageFilter CreatePointLitSpecular (SKPoint3 location, SKColor l public static SKImageFilter CreatePointLitSpecular (SKPoint3 location, SKColor lightColor, float surfaceScale, float ks, float shininess, SKImageFilter? input, SKRect cropRect) => CreatePointLitSpecular (location, lightColor, surfaceScale, ks, shininess, input, &cropRect); - private static SKImageFilter CreatePointLitSpecular (SKPoint3 location, SKColor lightColor, float surfaceScale, float ks, float shininess, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_point_lit_specular (&location, (uint)lightColor, surfaceScale, ks, shininess, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreatePointLitSpecular (SKPoint3 location, SKColor lightColor, float surfaceScale, float ks, float shininess, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_point_lit_specular (&location, (uint)lightColor, surfaceScale, ks, shininess, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreateSpotLitSpecular @@ -209,8 +253,12 @@ public static SKImageFilter CreateSpotLitSpecular (SKPoint3 location, SKPoint3 t public static SKImageFilter CreateSpotLitSpecular (SKPoint3 location, SKPoint3 target, float specularExponent, float cutoffAngle, SKColor lightColor, float surfaceScale, float ks, float shininess, SKImageFilter? input, SKRect cropRect) => CreateSpotLitSpecular (location, target, specularExponent, cutoffAngle, lightColor, surfaceScale, ks, shininess, input, &cropRect); - private static SKImageFilter CreateSpotLitSpecular (SKPoint3 location, SKPoint3 target, float specularExponent, float cutoffAngle, SKColor lightColor, float surfaceScale, float ks, float shininess, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_spot_lit_specular (&location, &target, specularExponent, cutoffAngle, (uint)lightColor, surfaceScale, ks, shininess, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateSpotLitSpecular (SKPoint3 location, SKPoint3 target, float specularExponent, float cutoffAngle, SKColor lightColor, float surfaceScale, float ks, float shininess, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_spot_lit_specular (&location, &target, specularExponent, cutoffAngle, (uint)lightColor, surfaceScale, ks, shininess, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreateMatrixConvolution @@ -228,7 +276,9 @@ private static SKImageFilter CreateMatrixConvolution (SKSizeI kernelSize, ReadOn if (kernel.Length != kernelSize.Width * kernelSize.Height) throw new ArgumentException ("Kernel length must match the dimensions of the kernel size (Width * Height).", nameof (kernel)); fixed (float* k = kernel) { - return GetObject (SkiaApi.sk_imagefilter_new_matrix_convolution (&kernelSize, k, gain, bias, &kernelOffset, tileMode, convolveAlpha, input?.Handle ?? IntPtr.Zero, cropRect)); + var filter = GetObject (SkiaApi.sk_imagefilter_new_matrix_convolution (&kernelSize, k, gain, bias, &kernelOffset, tileMode, convolveAlpha, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; } } @@ -240,8 +290,13 @@ public static SKImageFilter CreateMerge (SKImageFilter? first, SKImageFilter? se public static SKImageFilter CreateMerge (SKImageFilter? first, SKImageFilter? second, SKRect cropRect) => CreateMerge (first, second, &cropRect); - private static SKImageFilter CreateMerge (SKImageFilter? first, SKImageFilter? second, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_merge_simple (first?.Handle ?? IntPtr.Zero, second?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateMerge (SKImageFilter? first, SKImageFilter? second, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_merge_simple (first?.Handle ?? IntPtr.Zero, second?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (first); + GC.KeepAlive (second); + return filter; + } public static SKImageFilter CreateMerge (ReadOnlySpan filters) => CreateMerge (filters, null); @@ -271,8 +326,12 @@ public static SKImageFilter CreateDilate (float radiusX, float radiusY, SKImageF public static SKImageFilter CreateDilate (float radiusX, float radiusY, SKImageFilter? input, SKRect cropRect) => CreateDilate (radiusX, radiusY, input, &cropRect); - private static SKImageFilter CreateDilate (float radiusX, float radiusY, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_dilate (radiusX, radiusY, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateDilate (float radiusX, float radiusY, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_dilate (radiusX, radiusY, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreateErode @@ -285,8 +344,12 @@ public static SKImageFilter CreateErode (float radiusX, float radiusY, SKImageFi public static SKImageFilter CreateErode (float radiusX, float radiusY, SKImageFilter? input, SKRect cropRect) => CreateErode (radiusX, radiusY, input, &cropRect); - private static SKImageFilter CreateErode (float radiusX, float radiusY, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_erode (radiusX, radiusY, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateErode (float radiusX, float radiusY, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_erode (radiusX, radiusY, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreateOffset @@ -299,21 +362,29 @@ public static SKImageFilter CreateOffset (float radiusX, float radiusY, SKImageF public static SKImageFilter CreateOffset (float radiusX, float radiusY, SKImageFilter? input, SKRect cropRect) => CreateOffset (radiusX, radiusY, input, &cropRect); - private static SKImageFilter CreateOffset (float radiusX, float radiusY, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_offset (radiusX, radiusY, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateOffset (float radiusX, float radiusY, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_offset (radiusX, radiusY, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreatePicture public static SKImageFilter CreatePicture (SKPicture picture) { _ = picture ?? throw new ArgumentNullException (nameof (picture)); - return GetObject (SkiaApi.sk_imagefilter_new_picture (picture.Handle)); + var filter = GetObject (SkiaApi.sk_imagefilter_new_picture (picture.Handle)); + GC.KeepAlive (picture); + return filter; } public static SKImageFilter CreatePicture (SKPicture picture, SKRect cropRect) { _ = picture ?? throw new ArgumentNullException (nameof (picture)); - return GetObject (SkiaApi.sk_imagefilter_new_picture_with_rect (picture.Handle, &cropRect)); + var filter = GetObject (SkiaApi.sk_imagefilter_new_picture_with_rect (picture.Handle, &cropRect)); + GC.KeepAlive (picture); + return filter; } // CreateTile @@ -324,7 +395,9 @@ public static SKImageFilter CreateTile (SKRect src, SKRect dst) => public static SKImageFilter CreateTile (SKRect src, SKRect dst, SKImageFilter? input) { _ = input ?? throw new ArgumentNullException (nameof (input)); - return GetObject (SkiaApi.sk_imagefilter_new_tile (&src, &dst, input.Handle)); + var filter = GetObject (SkiaApi.sk_imagefilter_new_tile (&src, &dst, input.Handle)); + GC.KeepAlive (input); + return filter; } // CreateBlendMode @@ -338,8 +411,13 @@ public static SKImageFilter CreateBlendMode (SKBlendMode mode, SKImageFilter? ba public static SKImageFilter CreateBlendMode (SKBlendMode mode, SKImageFilter? background, SKImageFilter? foreground, SKRect cropRect) => CreateBlendMode (mode, background, foreground, &cropRect); - private static SKImageFilter CreateBlendMode (SKBlendMode mode, SKImageFilter? background, SKImageFilter? foreground, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_blend (mode, background?.Handle ?? IntPtr.Zero, foreground?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateBlendMode (SKBlendMode mode, SKImageFilter? background, SKImageFilter? foreground, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_blend (mode, background?.Handle ?? IntPtr.Zero, foreground?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (background); + GC.KeepAlive (foreground); + return filter; + } // CreateBlendMode (Blender) @@ -355,7 +433,11 @@ public static SKImageFilter CreateBlendMode (SKBlender blender, SKImageFilter? b private static SKImageFilter CreateBlendMode (SKBlender blender, SKImageFilter? background, SKImageFilter? foreground, SKRect* cropRect) { _ = blender ?? throw new ArgumentNullException (nameof (blender)); - return GetObject (SkiaApi.sk_imagefilter_new_blender (blender.Handle, background?.Handle ?? IntPtr.Zero, foreground?.Handle ?? IntPtr.Zero, cropRect)); + var filter = GetObject (SkiaApi.sk_imagefilter_new_blender (blender.Handle, background?.Handle ?? IntPtr.Zero, foreground?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (blender); + GC.KeepAlive (background); + GC.KeepAlive (foreground); + return filter; } // CreateArithmetic @@ -369,8 +451,13 @@ public static SKImageFilter CreateArithmetic (float k1, float k2, float k3, floa public static SKImageFilter CreateArithmetic (float k1, float k2, float k3, float k4, bool enforcePMColor, SKImageFilter? background, SKImageFilter? foreground, SKRect cropRect) => CreateArithmetic (k1, k2, k3, k4, enforcePMColor, background, foreground, &cropRect); - private static SKImageFilter CreateArithmetic (float k1, float k2, float k3, float k4, bool enforcePMColor, SKImageFilter? background, SKImageFilter? foreground, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_arithmetic (k1, k2, k3, k4, enforcePMColor, background?.Handle ?? IntPtr.Zero, foreground?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateArithmetic (float k1, float k2, float k3, float k4, bool enforcePMColor, SKImageFilter? background, SKImageFilter? foreground, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_arithmetic (k1, k2, k3, k4, enforcePMColor, background?.Handle ?? IntPtr.Zero, foreground?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (background); + GC.KeepAlive (foreground); + return filter; + } // CreateImage @@ -380,13 +467,17 @@ public static SKImageFilter CreateImage (SKImage image) => public static SKImageFilter CreateImage (SKImage image, SKSamplingOptions sampling) { _ = image ?? throw new ArgumentNullException (nameof (image)); - return GetObject (SkiaApi.sk_imagefilter_new_image_simple (image.Handle, &sampling)); + var filter = GetObject (SkiaApi.sk_imagefilter_new_image_simple (image.Handle, &sampling)); + GC.KeepAlive (image); + return filter; } public static SKImageFilter CreateImage (SKImage image, SKRect src, SKRect dst, SKSamplingOptions sampling) { _ = image ?? throw new ArgumentNullException (nameof (image)); - return GetObject (SkiaApi.sk_imagefilter_new_image (image.Handle, &src, &dst, &sampling)); + var filter = GetObject (SkiaApi.sk_imagefilter_new_image (image.Handle, &src, &dst, &sampling)); + GC.KeepAlive (image); + return filter; } [Obsolete("Use CreateImage(SKImage, SKRect, SKRect, SKSamplingOptions) instead.", true)] @@ -404,8 +495,12 @@ public static SKImageFilter CreateMagnifier (SKRect lensBounds, float zoomAmount public static SKImageFilter CreateMagnifier (SKRect lensBounds, float zoomAmount, float inset, SKSamplingOptions sampling, SKImageFilter? input, SKRect cropRect) => CreateMagnifier (lensBounds, zoomAmount, inset, sampling, input, &cropRect); - private static SKImageFilter CreateMagnifier (SKRect lensBounds, float zoomAmount, float inset, SKSamplingOptions sampling, SKImageFilter? input, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_magnifier (&lensBounds, zoomAmount, inset, &sampling, input?.Handle ?? IntPtr.Zero, cropRect)); + private static SKImageFilter CreateMagnifier (SKRect lensBounds, float zoomAmount, float inset, SKSamplingOptions sampling, SKImageFilter? input, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_magnifier (&lensBounds, zoomAmount, inset, &sampling, input?.Handle ?? IntPtr.Zero, cropRect)); + GC.KeepAlive (input); + return filter; + } // CreatePaint @@ -434,8 +529,12 @@ public static SKImageFilter CreateShader (SKShader? shader, bool dither) => public static SKImageFilter CreateShader (SKShader? shader, bool dither, SKRect cropRect) => CreateShader (shader, dither, &cropRect); - private static SKImageFilter CreateShader (SKShader? shader, bool dither, SKRect* cropRect) => - GetObject (SkiaApi.sk_imagefilter_new_shader (shader?.Handle ?? IntPtr.Zero, dither, cropRect)); + private static SKImageFilter CreateShader (SKShader? shader, bool dither, SKRect* cropRect) + { + var filter = GetObject (SkiaApi.sk_imagefilter_new_shader (shader?.Handle ?? IntPtr.Zero, dither, cropRect)); + GC.KeepAlive (shader); + return filter; + } // diff --git a/binding/SkiaSharp/SKMaskFilter.cs b/binding/SkiaSharp/SKMaskFilter.cs index 6f4550dd8b0..66c8b2afc62 100644 --- a/binding/SkiaSharp/SKMaskFilter.cs +++ b/binding/SkiaSharp/SKMaskFilter.cs @@ -67,7 +67,9 @@ public static SKMaskFilter CreateShader (SKShader shader) if (shader == null) throw new ArgumentNullException (nameof (shader)); - return GetObject (SkiaApi.sk_maskfilter_new_shader (shader.Handle)); + var maskFilter = GetObject (SkiaApi.sk_maskfilter_new_shader (shader.Handle)); + GC.KeepAlive (shader); + return maskFilter; } internal static SKMaskFilter GetObject (IntPtr handle) => diff --git a/binding/SkiaSharp/SKNWayCanvas.cs b/binding/SkiaSharp/SKNWayCanvas.cs index 20ff7297411..df0b8f5f2e6 100644 --- a/binding/SkiaSharp/SKNWayCanvas.cs +++ b/binding/SkiaSharp/SKNWayCanvas.cs @@ -23,6 +23,7 @@ public void AddCanvas (SKCanvas canvas) throw new ArgumentNullException (nameof (canvas)); SkiaApi.sk_nway_canvas_add_canvas (Handle, canvas.Handle); + GC.KeepAlive (canvas); } public void RemoveCanvas (SKCanvas canvas) @@ -31,6 +32,7 @@ public void RemoveCanvas (SKCanvas canvas) throw new ArgumentNullException (nameof (canvas)); SkiaApi.sk_nway_canvas_remove_canvas (Handle, canvas.Handle); + GC.KeepAlive (canvas); } public void RemoveAll () diff --git a/binding/SkiaSharp/SKOverdrawCanvas.cs b/binding/SkiaSharp/SKOverdrawCanvas.cs index ca744f8e54c..564ea63b03c 100644 --- a/binding/SkiaSharp/SKOverdrawCanvas.cs +++ b/binding/SkiaSharp/SKOverdrawCanvas.cs @@ -18,6 +18,7 @@ public SKOverdrawCanvas (SKCanvas canvas) throw new ArgumentNullException (nameof (canvas)); Handle = SkiaApi.sk_overdraw_canvas_new (canvas.Handle); + GC.KeepAlive (canvas); } } } diff --git a/binding/SkiaSharp/SKPaint.cs b/binding/SkiaSharp/SKPaint.cs index 3896756bea5..06887150246 100644 --- a/binding/SkiaSharp/SKPaint.cs +++ b/binding/SkiaSharp/SKPaint.cs @@ -87,6 +87,7 @@ public SKPaint (SKFont font) throw new ArgumentNullException (nameof (font)); Handle = SkiaApi.sk_compatpaint_new_with_font (font.Handle); + GC.KeepAlive (font); if (Handle == IntPtr.Zero) throw new InvalidOperationException ("Unable to create a new SKPaint instance."); @@ -95,24 +96,38 @@ public SKPaint (SKFont font) protected override void Dispose (bool disposing) => base.Dispose (disposing); - protected override void DisposeNative () => + protected override void DisposeNative () + { SkiaApi.sk_compatpaint_delete (Handle); + } // Reset - public void Reset () => + public void Reset () + { SkiaApi.sk_compatpaint_reset (Handle, DefaultFont.Handle); + } // properties public bool IsAntialias { - get => SkiaApi.sk_paint_is_antialias (Handle); - set => SkiaApi.sk_compatpaint_set_is_antialias (Handle, value); + get { + var r = SkiaApi.sk_paint_is_antialias (Handle); + return r; + } + set { + SkiaApi.sk_compatpaint_set_is_antialias (Handle, value); + } } public bool IsDither { - get => SkiaApi.sk_paint_is_dither (Handle); - set => SkiaApi.sk_paint_set_dither (Handle, value); + get { + var r = SkiaApi.sk_paint_is_dither (Handle); + return r; + } + set { + SkiaApi.sk_paint_set_dither (Handle, value); + } } [Obsolete ($"Use {nameof (SKFont)}.{nameof (SKFont.LinearMetrics)} instead.", error: true)] @@ -129,8 +144,13 @@ public bool SubpixelText { [Obsolete ($"Use {nameof (SKFont)}.{nameof (SKFont.Edging)} instead.", error: true)] public bool LcdRenderText { - get => SkiaApi.sk_compatpaint_get_lcd_render_text (Handle); - set => SkiaApi.sk_compatpaint_set_lcd_render_text (Handle, value); + get { + var r = SkiaApi.sk_compatpaint_get_lcd_render_text (Handle); + return r; + } + set { + SkiaApi.sk_compatpaint_set_lcd_render_text (Handle, value); + } } [Obsolete ($"Use {nameof (SKFont)}.{nameof (SKFont.EmbeddedBitmaps)} instead.", error: true)] @@ -163,13 +183,23 @@ public bool IsStroke { } public SKPaintStyle Style { - get => SkiaApi.sk_paint_get_style (Handle); - set => SkiaApi.sk_paint_set_style (Handle, value); + get { + var r = SkiaApi.sk_paint_get_style (Handle); + return r; + } + set { + SkiaApi.sk_paint_set_style (Handle, value); + } } public SKColor Color { - get => SkiaApi.sk_paint_get_color (Handle); - set => SkiaApi.sk_paint_set_color (Handle, (uint)value); + get { + var r = SkiaApi.sk_paint_get_color (Handle); + return r; + } + set { + SkiaApi.sk_paint_set_color (Handle, (uint)value); + } } public SKColorF ColorF { @@ -178,66 +208,131 @@ public SKColorF ColorF { SkiaApi.sk_paint_get_color4f (Handle, &color4f); return color4f; } - set => SkiaApi.sk_paint_set_color4f (Handle, &value, IntPtr.Zero); + set { + SkiaApi.sk_paint_set_color4f (Handle, &value, IntPtr.Zero); + } } - public void SetColor (SKColorF color, SKColorSpace colorspace) => + public void SetColor (SKColorF color, SKColorSpace colorspace) + { SkiaApi.sk_paint_set_color4f (Handle, &color, colorspace?.Handle ?? IntPtr.Zero); + GC.KeepAlive (colorspace); + } public float StrokeWidth { - get => SkiaApi.sk_paint_get_stroke_width (Handle); - set => SkiaApi.sk_paint_set_stroke_width (Handle, value); + get { + var r = SkiaApi.sk_paint_get_stroke_width (Handle); + return r; + } + set { + SkiaApi.sk_paint_set_stroke_width (Handle, value); + } } public float StrokeMiter { - get => SkiaApi.sk_paint_get_stroke_miter (Handle); - set => SkiaApi.sk_paint_set_stroke_miter (Handle, value); + get { + var r = SkiaApi.sk_paint_get_stroke_miter (Handle); + return r; + } + set { + SkiaApi.sk_paint_set_stroke_miter (Handle, value); + } } public SKStrokeCap StrokeCap { - get => SkiaApi.sk_paint_get_stroke_cap (Handle); - set => SkiaApi.sk_paint_set_stroke_cap (Handle, value); + get { + var r = SkiaApi.sk_paint_get_stroke_cap (Handle); + return r; + } + set { + SkiaApi.sk_paint_set_stroke_cap (Handle, value); + } } public SKStrokeJoin StrokeJoin { - get => SkiaApi.sk_paint_get_stroke_join (Handle); - set => SkiaApi.sk_paint_set_stroke_join (Handle, value); + get { + var r = SkiaApi.sk_paint_get_stroke_join (Handle); + return r; + } + set { + SkiaApi.sk_paint_set_stroke_join (Handle, value); + } } public SKShader Shader { - get => SKShader.GetObject (SkiaApi.sk_paint_get_shader (Handle)); - set => SkiaApi.sk_paint_set_shader (Handle, value == null ? IntPtr.Zero : value.Handle); + get { + var r = SKShader.GetObject (SkiaApi.sk_paint_get_shader (Handle)); + return r; + } + set { + SkiaApi.sk_paint_set_shader (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } public SKMaskFilter MaskFilter { - get => SKMaskFilter.GetObject (SkiaApi.sk_paint_get_maskfilter (Handle)); - set => SkiaApi.sk_paint_set_maskfilter (Handle, value == null ? IntPtr.Zero : value.Handle); + get { + var r = SKMaskFilter.GetObject (SkiaApi.sk_paint_get_maskfilter (Handle)); + return r; + } + set { + SkiaApi.sk_paint_set_maskfilter (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } public SKColorFilter ColorFilter { - get => SKColorFilter.GetObject (SkiaApi.sk_paint_get_colorfilter (Handle)); - set => SkiaApi.sk_paint_set_colorfilter (Handle, value == null ? IntPtr.Zero : value.Handle); + get { + var r = SKColorFilter.GetObject (SkiaApi.sk_paint_get_colorfilter (Handle)); + return r; + } + set { + SkiaApi.sk_paint_set_colorfilter (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } public SKImageFilter ImageFilter { - get => SKImageFilter.GetObject (SkiaApi.sk_paint_get_imagefilter (Handle)); - set => SkiaApi.sk_paint_set_imagefilter (Handle, value == null ? IntPtr.Zero : value.Handle); + get { + var r = SKImageFilter.GetObject (SkiaApi.sk_paint_get_imagefilter (Handle)); + return r; + } + set { + SkiaApi.sk_paint_set_imagefilter (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } public SKBlendMode BlendMode { - get => SkiaApi.sk_paint_get_blendmode (Handle); - set => SkiaApi.sk_paint_set_blendmode (Handle, value); + get { + var r = SkiaApi.sk_paint_get_blendmode (Handle); + return r; + } + set { + SkiaApi.sk_paint_set_blendmode (Handle, value); + } } public SKBlender Blender { - get => SKBlender.GetObject (SkiaApi.sk_paint_get_blender (Handle)); - set => SkiaApi.sk_paint_set_blender (Handle, value == null ? IntPtr.Zero : value.Handle); + get { + var r = SKBlender.GetObject (SkiaApi.sk_paint_get_blender (Handle)); + return r; + } + set { + SkiaApi.sk_paint_set_blender (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } [Obsolete ($"Use {nameof (SKSamplingOptions)} instead.", error: true)] public SKFilterQuality FilterQuality { - get => (SKFilterQuality)SkiaApi.sk_compatpaint_get_filter_quality (Handle); - set => SkiaApi.sk_compatpaint_set_filter_quality (Handle, (int)value); + get { + var r = (SKFilterQuality)SkiaApi.sk_compatpaint_get_filter_quality (Handle); + return r; + } + set { + SkiaApi.sk_compatpaint_set_filter_quality (Handle, (int)value); + } } [Obsolete ($"Use {nameof (SKFont)}.{nameof (SKFont.Typeface)} instead.", error: true)] @@ -254,14 +349,24 @@ public float TextSize { [Obsolete ($"Use {nameof (SKTextAlign)} method overloads instead.", error: true)] public SKTextAlign TextAlign { - get => SkiaApi.sk_compatpaint_get_text_align (Handle); - set => SkiaApi.sk_compatpaint_set_text_align (Handle, value); + get { + var r = SkiaApi.sk_compatpaint_get_text_align (Handle); + return r; + } + set { + SkiaApi.sk_compatpaint_set_text_align (Handle, value); + } } [Obsolete ($"Use {nameof (SKTextEncoding)} method overloads instead.", error: true)] public SKTextEncoding TextEncoding { - get => SkiaApi.sk_compatpaint_get_text_encoding (Handle); - set => SkiaApi.sk_compatpaint_set_text_encoding (Handle, value); + get { + var r = SkiaApi.sk_compatpaint_get_text_encoding (Handle); + return r; + } + set { + SkiaApi.sk_compatpaint_set_text_encoding (Handle, value); + } } [Obsolete ($"Use {nameof (SKFont)}.{nameof (SKFont.ScaleX)} instead.", error: true)] @@ -277,8 +382,14 @@ public float TextSkewX { } public SKPathEffect PathEffect { - get => SKPathEffect.GetObject (SkiaApi.sk_paint_get_path_effect (Handle)); - set => SkiaApi.sk_paint_set_path_effect (Handle, value == null ? IntPtr.Zero : value.Handle); + get { + var r = SKPathEffect.GetObject (SkiaApi.sk_paint_get_path_effect (Handle)); + return r; + } + set { + SkiaApi.sk_paint_set_path_effect (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } // FontSpacing @@ -302,8 +413,11 @@ public float GetFontMetrics (out SKFontMetrics metrics) => // Clone - public SKPaint Clone () => - GetObject (SkiaApi.sk_compatpaint_clone (Handle))!; + public SKPaint Clone () + { + var r = GetObject (SkiaApi.sk_compatpaint_clone (Handle))!; + return r; + } // MeasureText @@ -568,7 +682,10 @@ private bool GetFillPath (SKPath src, SKPathBuilder dst, SKRect* cullRect, SKMat _ = src ?? throw new ArgumentNullException (nameof (src)); _ = dst ?? throw new ArgumentNullException (nameof (dst)); - return SkiaApi.sk_paint_get_fill_path (Handle, src.Handle, dst.Handle, cullRect, &matrix); + var result = SkiaApi.sk_paint_get_fill_path (Handle, src.Handle, dst.Handle, cullRect, &matrix); + GC.KeepAlive (src); + GC.KeepAlive (dst); + return result; } // CountGlyphs @@ -863,8 +980,11 @@ public float[] GetHorizontalTextIntercepts (IntPtr text, IntPtr length, float[] // Font [Obsolete ($"Use {nameof (SKFont)} instead.", error: true)] - public SKFont ToFont () => - SKFont.GetObject (SkiaApi.sk_compatpaint_make_font (Handle)); + public SKFont ToFont () + { + var r = SKFont.GetObject (SkiaApi.sk_compatpaint_make_font (Handle)); + return r; + } [Obsolete ($"Use {nameof (SKFont)} instead.", error: true)] internal SKFont GetFont () => diff --git a/binding/SkiaSharp/SKPath.cs b/binding/SkiaSharp/SKPath.cs index 85091075007..0e55bebd74c 100644 --- a/binding/SkiaSharp/SKPath.cs +++ b/binding/SkiaSharp/SKPath.cs @@ -52,6 +52,7 @@ public SKPath () public SKPath (SKPath path) : this (SkiaApi.sk_path_clone (path.Handle), true) { + GC.KeepAlive (path); if (Handle == IntPtr.Zero) { throw new InvalidOperationException ("Unable to copy the SKPath instance."); } @@ -68,7 +69,10 @@ protected override void DisposeNative () } public SKPathFillType FillType { - get => SkiaApi.sk_path_get_filltype (Handle); + get { + var r = SkiaApi.sk_path_get_filltype (Handle); + return r; + } set { SkiaApi.sk_path_set_filltype (Handle, value); if (_builder != null) @@ -78,25 +82,25 @@ public SKPathFillType FillType { public SKPathConvexity Convexity { get { return IsConvex ? SKPathConvexity.Convex : SKPathConvexity.Concave; } } - public bool IsConvex { get { return SkiaApi.sk_path_is_convex (Handle); } } + public bool IsConvex { get { var r = SkiaApi.sk_path_is_convex (Handle); GC.KeepAlive (this); return r; } } public bool IsConcave => !IsConvex; public bool IsEmpty => VerbCount == 0; - public bool IsOval { get { return SkiaApi.sk_path_is_oval (Handle, null); } } + public bool IsOval { get { var r = SkiaApi.sk_path_is_oval (Handle, null); GC.KeepAlive (this); return r; } } - public bool IsRoundRect { get { return SkiaApi.sk_path_is_rrect (Handle, IntPtr.Zero); } } + public bool IsRoundRect { get { var r = SkiaApi.sk_path_is_rrect (Handle, IntPtr.Zero); GC.KeepAlive (this); return r; } } - public bool IsLine { get { return SkiaApi.sk_path_is_line (Handle, null); } } + public bool IsLine { get { var r = SkiaApi.sk_path_is_line (Handle, null); GC.KeepAlive (this); return r; } } - public bool IsRect { get { return SkiaApi.sk_path_is_rect (Handle, null, null, null); } } + public bool IsRect { get { var r = SkiaApi.sk_path_is_rect (Handle, null, null, null); GC.KeepAlive (this); return r; } } - public SKPathSegmentMask SegmentMasks { get { return (SKPathSegmentMask)SkiaApi.sk_path_get_segment_masks (Handle); } } + public SKPathSegmentMask SegmentMasks { get { var r = (SKPathSegmentMask)SkiaApi.sk_path_get_segment_masks (Handle); GC.KeepAlive (this); return r; } } - public int VerbCount { get { return SkiaApi.sk_path_count_verbs (Handle); } } + public int VerbCount { get { var r = SkiaApi.sk_path_count_verbs (Handle); GC.KeepAlive (this); return r; } } - public int PointCount { get { return SkiaApi.sk_path_count_points (Handle); } } + public int PointCount { get { var r = SkiaApi.sk_path_count_points (Handle); GC.KeepAlive (this); return r; } } public SKPoint this[int index] => GetPoint (index); @@ -131,7 +135,8 @@ public SKRect TightBounds { public SKRect GetOvalBounds () { SKRect bounds; - if (SkiaApi.sk_path_is_oval (Handle, &bounds)) { + var isOval = SkiaApi.sk_path_is_oval (Handle, &bounds); + if (isOval) { return bounds; } else { return SKRect.Empty; @@ -201,13 +206,15 @@ public SKPoint[] GetPoints (int max) public int GetPoints (SKPoint[] points, int max) { fixed (SKPoint* p = points) { - return SkiaApi.sk_path_get_points (Handle, p, max); + var r = SkiaApi.sk_path_get_points (Handle, p, max); + return r; } } public bool Contains (float x, float y) { - return SkiaApi.sk_path_contains (Handle, x, y); + var r = SkiaApi.sk_path_contains (Handle, x, y); + return r; } public void Offset (SKPoint offset) => @@ -250,8 +257,9 @@ public SKRect ComputeTightBounds () public void Transform (in SKMatrix matrix) { - fixed (SKMatrix* m = &matrix) + fixed (SKMatrix* m = &matrix) { SkiaApi.sk_path_transform (Handle, m); + } } public void Transform (in SKMatrix matrix, SKPath destination) @@ -261,6 +269,7 @@ public void Transform (in SKMatrix matrix, SKPath destination) fixed (SKMatrix* m = &matrix) SkiaApi.sk_path_transform_to_dest (Handle, m, destination.Handle); + GC.KeepAlive (destination); } [Obsolete("Use Transform(in SKMatrix) instead.", true)] @@ -288,7 +297,10 @@ public bool Op (SKPath other, SKPathOp op, SKPath result) if (result == null) throw new ArgumentNullException (nameof (result)); - return SkiaApi.sk_pathop_op (Handle, other.Handle, op, result.Handle); + var success = SkiaApi.sk_pathop_op (Handle, other.Handle, op, result.Handle); + GC.KeepAlive (other); + GC.KeepAlive (result); + return success; } public SKPath Op (SKPath other, SKPathOp op) @@ -307,7 +319,9 @@ public bool Simplify (SKPath result) if (result == null) throw new ArgumentNullException (nameof (result)); - return SkiaApi.sk_pathop_simplify (Handle, result.Handle); + var success = SkiaApi.sk_pathop_simplify (Handle, result.Handle); + GC.KeepAlive (result); + return success; } public SKPath Simplify () @@ -324,7 +338,8 @@ public SKPath Simplify () public bool GetTightBounds (out SKRect result) { fixed (SKRect* r = &result) { - return SkiaApi.sk_pathop_tight_bounds (Handle, r); + var success = SkiaApi.sk_pathop_tight_bounds (Handle, r); + return success; } } @@ -333,7 +348,9 @@ public bool ToWinding (SKPath result) if (result == null) throw new ArgumentNullException (nameof (result)); - return SkiaApi.sk_pathop_as_winding (Handle, result.Handle); + var success = SkiaApi.sk_pathop_as_winding (Handle, result.Handle); + GC.KeepAlive (result); + return success; } public SKPath ToWinding () @@ -420,6 +437,7 @@ internal void ReplaceFromBuilder (SKPathBuilder builder) _builder = null; } var newHandle = SkiaApi.sk_pathbuilder_detach_path (builder.Handle); + GC.KeepAlive (builder); SkiaApi.sk_path_delete (Handle); Handle = newHandle; } @@ -782,18 +800,28 @@ public SKPathVerb Next (Span points) throw new ArgumentException ("Must be an array of four elements.", nameof (points)); fixed (SKPoint* p = points) { - return SkiaApi.sk_path_iter_next (Handle, p); + var r = SkiaApi.sk_path_iter_next (Handle, p); + return r; } } - public float ConicWeight () => - SkiaApi.sk_path_iter_conic_weight (Handle); + public float ConicWeight () + { + var r = SkiaApi.sk_path_iter_conic_weight (Handle); + return r; + } - public bool IsCloseLine () => - SkiaApi.sk_path_iter_is_close_line (Handle) != 0; + public bool IsCloseLine () + { + var r = SkiaApi.sk_path_iter_is_close_line (Handle) != 0; + return r; + } - public bool IsCloseContour () => - SkiaApi.sk_path_iter_is_closed_contour (Handle) != 0; + public bool IsCloseContour () + { + var r = SkiaApi.sk_path_iter_is_closed_contour (Handle) != 0; + return r; + } } public class RawIterator : SKObject, ISKSkipObjectRegistration @@ -820,15 +848,22 @@ public SKPathVerb Next (Span points) if (points.Length != 4) throw new ArgumentException ("Must be an array of four elements.", nameof (points)); fixed (SKPoint* p = points) { - return SkiaApi.sk_path_rawiter_next (Handle, p); + var r = SkiaApi.sk_path_rawiter_next (Handle, p); + return r; } } - public float ConicWeight () => - SkiaApi.sk_path_rawiter_conic_weight (Handle); + public float ConicWeight () + { + var r = SkiaApi.sk_path_rawiter_conic_weight (Handle); + return r; + } - public SKPathVerb Peek () => - SkiaApi.sk_path_rawiter_peek (Handle); + public SKPathVerb Peek () + { + var r = SkiaApi.sk_path_rawiter_peek (Handle); + return r; + } } public class OpBuilder : SKObject, ISKSkipObjectRegistration @@ -838,15 +873,20 @@ public OpBuilder () { } - public void Add (SKPath path, SKPathOp op) => + public void Add (SKPath path, SKPathOp op) + { SkiaApi.sk_opbuilder_add (Handle, path.Handle, op); + GC.KeepAlive (path); + } public bool Resolve (SKPath result) { if (result == null) throw new ArgumentNullException (nameof (result)); - return SkiaApi.sk_opbuilder_resolve (Handle, result.Handle); + var success = SkiaApi.sk_opbuilder_resolve (Handle, result.Handle); + GC.KeepAlive (result); + return success; } protected override void Dispose (bool disposing) => diff --git a/binding/SkiaSharp/SKPathBuilder.cs b/binding/SkiaSharp/SKPathBuilder.cs index 3a4b1be94bb..378e48527fa 100644 --- a/binding/SkiaSharp/SKPathBuilder.cs +++ b/binding/SkiaSharp/SKPathBuilder.cs @@ -22,6 +22,7 @@ public SKPathBuilder () public SKPathBuilder (SKPath path) : this (SkiaApi.sk_pathbuilder_new_from_path (path?.Handle ?? throw new ArgumentNullException (nameof (path))), true) { + GC.KeepAlive (path); if (Handle == IntPtr.Zero) { throw new InvalidOperationException ("Unable to create a new SKPathBuilder instance."); } @@ -34,121 +35,192 @@ protected override void DisposeNative () => SkiaApi.sk_pathbuilder_delete (Handle); public SKPathFillType FillType { - get => SkiaApi.sk_pathbuilder_get_filltype (Handle); - set => SkiaApi.sk_pathbuilder_set_filltype (Handle, value); + get { + var r = SkiaApi.sk_pathbuilder_get_filltype (Handle); + return r; + } + set { + SkiaApi.sk_pathbuilder_set_filltype (Handle, value); + } } - public SKPath Detach () => - SKPath.GetObject (SkiaApi.sk_pathbuilder_detach_path (Handle)); + public SKPath Detach () + { + var r = SKPath.GetObject (SkiaApi.sk_pathbuilder_detach_path (Handle)); + return r; + } - public SKPath Snapshot () => - SKPath.GetObject (SkiaApi.sk_pathbuilder_snapshot_path (Handle)); + public SKPath Snapshot () + { + var r = SKPath.GetObject (SkiaApi.sk_pathbuilder_snapshot_path (Handle)); + return r; + } - public void Reset () => + public void Reset () + { SkiaApi.sk_pathbuilder_reset (Handle); + } // Move - public void MoveTo (SKPoint point) => + public void MoveTo (SKPoint point) + { SkiaApi.sk_pathbuilder_move_to (Handle, point.X, point.Y); + } - public void MoveTo (float x, float y) => + public void MoveTo (float x, float y) + { SkiaApi.sk_pathbuilder_move_to (Handle, x, y); + } - public void RMoveTo (SKPoint point) => + public void RMoveTo (SKPoint point) + { SkiaApi.sk_pathbuilder_rmove_to (Handle, point.X, point.Y); + } - public void RMoveTo (float dx, float dy) => + public void RMoveTo (float dx, float dy) + { SkiaApi.sk_pathbuilder_rmove_to (Handle, dx, dy); + } // Line - public void LineTo (SKPoint point) => + public void LineTo (SKPoint point) + { SkiaApi.sk_pathbuilder_line_to (Handle, point.X, point.Y); + } - public void LineTo (float x, float y) => + public void LineTo (float x, float y) + { SkiaApi.sk_pathbuilder_line_to (Handle, x, y); + } - public void RLineTo (SKPoint point) => + public void RLineTo (SKPoint point) + { SkiaApi.sk_pathbuilder_rline_to (Handle, point.X, point.Y); + } - public void RLineTo (float dx, float dy) => + public void RLineTo (float dx, float dy) + { SkiaApi.sk_pathbuilder_rline_to (Handle, dx, dy); + } // Quad - public void QuadTo (SKPoint point0, SKPoint point1) => + public void QuadTo (SKPoint point0, SKPoint point1) + { SkiaApi.sk_pathbuilder_quad_to (Handle, point0.X, point0.Y, point1.X, point1.Y); + } - public void QuadTo (float x0, float y0, float x1, float y1) => + public void QuadTo (float x0, float y0, float x1, float y1) + { SkiaApi.sk_pathbuilder_quad_to (Handle, x0, y0, x1, y1); + } - public void RQuadTo (SKPoint point0, SKPoint point1) => + public void RQuadTo (SKPoint point0, SKPoint point1) + { SkiaApi.sk_pathbuilder_rquad_to (Handle, point0.X, point0.Y, point1.X, point1.Y); + } - public void RQuadTo (float dx0, float dy0, float dx1, float dy1) => + public void RQuadTo (float dx0, float dy0, float dx1, float dy1) + { SkiaApi.sk_pathbuilder_rquad_to (Handle, dx0, dy0, dx1, dy1); + } // Conic - public void ConicTo (SKPoint point0, SKPoint point1, float w) => + public void ConicTo (SKPoint point0, SKPoint point1, float w) + { SkiaApi.sk_pathbuilder_conic_to (Handle, point0.X, point0.Y, point1.X, point1.Y, w); + } - public void ConicTo (float x0, float y0, float x1, float y1, float w) => + public void ConicTo (float x0, float y0, float x1, float y1, float w) + { SkiaApi.sk_pathbuilder_conic_to (Handle, x0, y0, x1, y1, w); + } - public void RConicTo (SKPoint point0, SKPoint point1, float w) => + public void RConicTo (SKPoint point0, SKPoint point1, float w) + { SkiaApi.sk_pathbuilder_rconic_to (Handle, point0.X, point0.Y, point1.X, point1.Y, w); + } - public void RConicTo (float dx0, float dy0, float dx1, float dy1, float w) => + public void RConicTo (float dx0, float dy0, float dx1, float dy1, float w) + { SkiaApi.sk_pathbuilder_rconic_to (Handle, dx0, dy0, dx1, dy1, w); + } // Cubic - public void CubicTo (SKPoint point0, SKPoint point1, SKPoint point2) => + public void CubicTo (SKPoint point0, SKPoint point1, SKPoint point2) + { SkiaApi.sk_pathbuilder_cubic_to (Handle, point0.X, point0.Y, point1.X, point1.Y, point2.X, point2.Y); + } - public void CubicTo (float x0, float y0, float x1, float y1, float x2, float y2) => + public void CubicTo (float x0, float y0, float x1, float y1, float x2, float y2) + { SkiaApi.sk_pathbuilder_cubic_to (Handle, x0, y0, x1, y1, x2, y2); + } - public void RCubicTo (SKPoint point0, SKPoint point1, SKPoint point2) => + public void RCubicTo (SKPoint point0, SKPoint point1, SKPoint point2) + { SkiaApi.sk_pathbuilder_rcubic_to (Handle, point0.X, point0.Y, point1.X, point1.Y, point2.X, point2.Y); + } - public void RCubicTo (float dx0, float dy0, float dx1, float dy1, float dx2, float dy2) => + public void RCubicTo (float dx0, float dy0, float dx1, float dy1, float dx2, float dy2) + { SkiaApi.sk_pathbuilder_rcubic_to (Handle, dx0, dy0, dx1, dy1, dx2, dy2); + } // Arc - public void ArcTo (SKPoint r, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, SKPoint xy) => + public void ArcTo (SKPoint r, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, SKPoint xy) + { SkiaApi.sk_pathbuilder_arc_to (Handle, r.X, r.Y, xAxisRotate, largeArc, sweep, xy.X, xy.Y); + } - public void ArcTo (float rx, float ry, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, float x, float y) => + public void ArcTo (float rx, float ry, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, float x, float y) + { SkiaApi.sk_pathbuilder_arc_to (Handle, rx, ry, xAxisRotate, largeArc, sweep, x, y); + } - public void ArcTo (SKRect oval, float startAngle, float sweepAngle, bool forceMoveTo) => + public void ArcTo (SKRect oval, float startAngle, float sweepAngle, bool forceMoveTo) + { SkiaApi.sk_pathbuilder_arc_to_with_oval (Handle, &oval, startAngle, sweepAngle, forceMoveTo); + } - public void ArcTo (SKPoint point1, SKPoint point2, float radius) => + public void ArcTo (SKPoint point1, SKPoint point2, float radius) + { SkiaApi.sk_pathbuilder_arc_to_with_points (Handle, point1.X, point1.Y, point2.X, point2.Y, radius); + } - public void ArcTo (float x1, float y1, float x2, float y2, float radius) => + public void ArcTo (float x1, float y1, float x2, float y2, float radius) + { SkiaApi.sk_pathbuilder_arc_to_with_points (Handle, x1, y1, x2, y2, radius); + } - public void RArcTo (SKPoint r, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, SKPoint xy) => + public void RArcTo (SKPoint r, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, SKPoint xy) + { SkiaApi.sk_pathbuilder_rarc_to (Handle, r.X, r.Y, xAxisRotate, largeArc, sweep, xy.X, xy.Y); + } - public void RArcTo (float rx, float ry, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, float x, float y) => + public void RArcTo (float rx, float ry, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, float x, float y) + { SkiaApi.sk_pathbuilder_rarc_to (Handle, rx, ry, xAxisRotate, largeArc, sweep, x, y); + } // Close - public void Close () => + public void Close () + { SkiaApi.sk_pathbuilder_close (Handle); + } // Add shapes - public void AddRect (SKRect rect, SKPathDirection direction = SKPathDirection.Clockwise) => + public void AddRect (SKRect rect, SKPathDirection direction = SKPathDirection.Clockwise) + { SkiaApi.sk_pathbuilder_add_rect (Handle, &rect, direction); + } public void AddRect (SKRect rect, SKPathDirection direction, uint startIndex) { @@ -163,6 +235,7 @@ public void AddRoundRect (SKRoundRect rect, SKPathDirection direction = SKPathDi if (rect == null) throw new ArgumentNullException (nameof (rect)); SkiaApi.sk_pathbuilder_add_rrect (Handle, rect.Handle, direction); + GC.KeepAlive (rect); } public void AddRoundRect (SKRoundRect rect, SKPathDirection direction, uint startIndex) @@ -170,19 +243,28 @@ public void AddRoundRect (SKRoundRect rect, SKPathDirection direction, uint star if (rect == null) throw new ArgumentNullException (nameof (rect)); SkiaApi.sk_pathbuilder_add_rrect_start (Handle, rect.Handle, direction, startIndex); + GC.KeepAlive (rect); } - public void AddOval (SKRect rect, SKPathDirection direction = SKPathDirection.Clockwise) => + public void AddOval (SKRect rect, SKPathDirection direction = SKPathDirection.Clockwise) + { SkiaApi.sk_pathbuilder_add_oval (Handle, &rect, direction); + } - public void AddArc (SKRect oval, float startAngle, float sweepAngle) => + public void AddArc (SKRect oval, float startAngle, float sweepAngle) + { SkiaApi.sk_pathbuilder_add_arc (Handle, &oval, startAngle, sweepAngle); + } - public void AddRoundRect (SKRect rect, float rx, float ry, SKPathDirection dir = SKPathDirection.Clockwise) => + public void AddRoundRect (SKRect rect, float rx, float ry, SKPathDirection dir = SKPathDirection.Clockwise) + { SkiaApi.sk_pathbuilder_add_rounded_rect (Handle, &rect, rx, ry, dir); + } - public void AddCircle (float x, float y, float radius, SKPathDirection dir = SKPathDirection.Clockwise) => + public void AddCircle (float x, float y, float radius, SKPathDirection dir = SKPathDirection.Clockwise) + { SkiaApi.sk_pathbuilder_add_circle (Handle, x, y, radius, dir); + } public void AddPoly (ReadOnlySpan points, bool close = true) { @@ -208,6 +290,7 @@ public void AddPath (SKPath other, float dx, float dy, SKPathAddMode mode = SKPa throw new ArgumentNullException (nameof (other)); SkiaApi.sk_pathbuilder_add_path_offset (Handle, other.Handle, dx, dy, mode); + GC.KeepAlive (other); } public void AddPath (SKPath other, in SKMatrix matrix, SKPathAddMode mode = SKPathAddMode.Append) @@ -217,6 +300,7 @@ public void AddPath (SKPath other, in SKMatrix matrix, SKPathAddMode mode = SKPa fixed (SKMatrix* m = &matrix) SkiaApi.sk_pathbuilder_add_path_matrix (Handle, other.Handle, m, mode); + GC.KeepAlive (other); } public void AddPath (SKPath other, SKPathAddMode mode = SKPathAddMode.Append) @@ -225,6 +309,7 @@ public void AddPath (SKPath other, SKPathAddMode mode = SKPathAddMode.Append) throw new ArgumentNullException (nameof (other)); SkiaApi.sk_pathbuilder_add_path (Handle, other.Handle, mode); + GC.KeepAlive (other); } public void ReverseAddPath (SKPath other) @@ -233,6 +318,7 @@ public void ReverseAddPath (SKPath other) throw new ArgumentNullException (nameof (other)); SkiaApi.sk_pathbuilder_reverse_add_path (Handle, other.Handle); + GC.KeepAlive (other); } } } diff --git a/binding/SkiaSharp/SKPathEffect.cs b/binding/SkiaSharp/SKPathEffect.cs index d21d3d7cc4f..8853ca910e7 100644 --- a/binding/SkiaSharp/SKPathEffect.cs +++ b/binding/SkiaSharp/SKPathEffect.cs @@ -20,7 +20,10 @@ public static SKPathEffect CreateCompose(SKPathEffect outer, SKPathEffect inner) throw new ArgumentNullException(nameof(outer)); if (inner == null) throw new ArgumentNullException(nameof(inner)); - return GetObject(SkiaApi.sk_path_effect_create_compose(outer.Handle, inner.Handle)); + var result = GetObject(SkiaApi.sk_path_effect_create_compose(outer.Handle, inner.Handle)); + GC.KeepAlive (outer); + GC.KeepAlive (inner); + return result; } public static SKPathEffect CreateSum(SKPathEffect first, SKPathEffect second) @@ -29,7 +32,10 @@ public static SKPathEffect CreateSum(SKPathEffect first, SKPathEffect second) throw new ArgumentNullException(nameof(first)); if (second == null) throw new ArgumentNullException(nameof(second)); - return GetObject(SkiaApi.sk_path_effect_create_sum(first.Handle, second.Handle)); + var result = GetObject(SkiaApi.sk_path_effect_create_sum(first.Handle, second.Handle)); + GC.KeepAlive (first); + GC.KeepAlive (second); + return result; } public static SKPathEffect CreateDiscrete(float segLength, float deviation, UInt32 seedAssist = 0) @@ -46,7 +52,9 @@ public static SKPathEffect Create1DPath(SKPath path, float advance, float phase, { if (path == null) throw new ArgumentNullException(nameof(path)); - return GetObject(SkiaApi.sk_path_effect_create_1d_path(path.Handle, advance, phase, style)); + var result = GetObject(SkiaApi.sk_path_effect_create_1d_path(path.Handle, advance, phase, style)); + GC.KeepAlive (path); + return result; } public static SKPathEffect Create2DLine(float width, SKMatrix matrix) @@ -58,7 +66,9 @@ public static SKPathEffect Create2DPath(SKMatrix matrix, SKPath path) { if (path == null) throw new ArgumentNullException(nameof(path)); - return GetObject(SkiaApi.sk_path_effect_create_2d_path(&matrix, path.Handle)); + var result = GetObject(SkiaApi.sk_path_effect_create_2d_path(&matrix, path.Handle)); + GC.KeepAlive (path); + return result; } public static SKPathEffect CreateDash(float[] intervals, float phase) diff --git a/binding/SkiaSharp/SKPathMeasure.cs b/binding/SkiaSharp/SKPathMeasure.cs index ff82651db2e..9f6986faf6c 100644 --- a/binding/SkiaSharp/SKPathMeasure.cs +++ b/binding/SkiaSharp/SKPathMeasure.cs @@ -26,6 +26,7 @@ public SKPathMeasure (SKPath path, bool forceClosed = false, float resScale = 1) throw new ArgumentNullException (nameof (path)); Handle = SkiaApi.sk_pathmeasure_new_with_path (path.Handle, forceClosed, resScale); + GC.KeepAlive (path); if (Handle == IntPtr.Zero) { throw new InvalidOperationException ("Unable to create a new SKPathMeasure instance."); @@ -42,13 +43,15 @@ protected override void DisposeNative () => public float Length { get { - return SkiaApi.sk_pathmeasure_get_length (Handle); + var r = SkiaApi.sk_pathmeasure_get_length (Handle); + return r; } } public bool IsClosed { get { - return SkiaApi.sk_pathmeasure_is_closed (Handle); + var r = SkiaApi.sk_pathmeasure_is_closed (Handle); + return r; } } @@ -60,6 +63,7 @@ public void SetPath (SKPath path) => public void SetPath (SKPath path, bool forceClosed) { SkiaApi.sk_pathmeasure_set_path (Handle, path == null ? IntPtr.Zero : path.Handle, forceClosed); + GC.KeepAlive (path); } // GetPositionAndTangent @@ -68,7 +72,8 @@ public bool GetPositionAndTangent (float distance, out SKPoint position, out SKP { fixed (SKPoint* p = &position) fixed (SKPoint* t = &tangent) { - return SkiaApi.sk_pathmeasure_get_pos_tan (Handle, distance, p, t); + var r = SkiaApi.sk_pathmeasure_get_pos_tan (Handle, distance, p, t); + return r; } } @@ -84,7 +89,8 @@ public SKPoint GetPosition (float distance) public bool GetPosition (float distance, out SKPoint position) { fixed (SKPoint* p = &position) { - return SkiaApi.sk_pathmeasure_get_pos_tan (Handle, distance, p, null); + var r = SkiaApi.sk_pathmeasure_get_pos_tan (Handle, distance, p, null); + return r; } } @@ -100,7 +106,8 @@ public SKPoint GetTangent (float distance) public bool GetTangent (float distance, out SKPoint tangent) { fixed (SKPoint* t = &tangent) { - return SkiaApi.sk_pathmeasure_get_pos_tan (Handle, distance, null, t); + var r = SkiaApi.sk_pathmeasure_get_pos_tan (Handle, distance, null, t); + return r; } } @@ -116,7 +123,8 @@ public SKMatrix GetMatrix (float distance, SKPathMeasureMatrixFlags flags) public bool GetMatrix (float distance, out SKMatrix matrix, SKPathMeasureMatrixFlags flags) { fixed (SKMatrix* m = &matrix) { - return SkiaApi.sk_pathmeasure_get_matrix (Handle, distance, m, flags); + var r = SkiaApi.sk_pathmeasure_get_matrix (Handle, distance, m, flags); + return r; } } @@ -126,7 +134,9 @@ public bool GetSegment (float start, float stop, SKPathBuilder dst, bool startWi { if (dst == null) throw new ArgumentNullException (nameof (dst)); - return SkiaApi.sk_pathmeasure_get_segment (Handle, start, stop, dst.Handle, startWithMoveTo); + var result = SkiaApi.sk_pathmeasure_get_segment (Handle, start, stop, dst.Handle, startWithMoveTo); + GC.KeepAlive (dst); + return result; } [Obsolete ("Use the SKPathBuilder overload instead.")] @@ -156,7 +166,8 @@ public SKPath GetSegment (float start, float stop, bool startWithMoveTo) public bool NextContour () { - return SkiaApi.sk_pathmeasure_next_contour (Handle); + var r = SkiaApi.sk_pathmeasure_next_contour (Handle); + return r; } } } diff --git a/binding/SkiaSharp/SKPicture.cs b/binding/SkiaSharp/SKPicture.cs index 4726dcebbc2..243feb3493a 100644 --- a/binding/SkiaSharp/SKPicture.cs +++ b/binding/SkiaSharp/SKPicture.cs @@ -15,8 +15,12 @@ internal SKPicture (IntPtr h, bool owns) protected override void Dispose (bool disposing) => base.Dispose (disposing); - public uint UniqueId => - SkiaApi.sk_picture_get_unique_id (Handle); + public uint UniqueId { + get { + var result = SkiaApi.sk_picture_get_unique_id (Handle); + return result; + } + } public SKRect CullRect { get { @@ -26,19 +30,29 @@ public SKRect CullRect { } } - public int ApproximateBytesUsed => - (int)SkiaApi.sk_picture_approximate_bytes_used (Handle); + public int ApproximateBytesUsed { + get { + var result = (int)SkiaApi.sk_picture_approximate_bytes_used (Handle); + return result; + } + } public int ApproximateOperationCount => GetApproximateOperationCount (false); - public int GetApproximateOperationCount(bool includeNested) => - SkiaApi.sk_picture_approximate_op_count (Handle, includeNested); + public int GetApproximateOperationCount(bool includeNested) + { + var result = SkiaApi.sk_picture_approximate_op_count (Handle, includeNested); + return result; + } // Serialize - public SKData Serialize () => - SKData.GetObject (SkiaApi.sk_picture_serialize_to_data (Handle)); + public SKData Serialize () + { + var result = SKData.GetObject (SkiaApi.sk_picture_serialize_to_data (Handle)); + return result; + } public void Serialize (Stream stream) { @@ -55,6 +69,7 @@ public void Serialize (SKWStream stream) throw new ArgumentNullException (nameof (stream)); SkiaApi.sk_picture_serialize_to_stream (Handle, stream.Handle); + GC.KeepAlive (stream); } // Playback @@ -65,6 +80,7 @@ public void Playback (SKCanvas canvas) throw new ArgumentNullException (nameof (canvas)); SkiaApi.sk_picture_playback (Handle, canvas.Handle); + GC.KeepAlive (canvas); } // ToShader @@ -90,8 +106,11 @@ public SKShader ToShader (SKShaderTileMode tmx, SKShaderTileMode tmy, SKMatrix l public SKShader ToShader (SKShaderTileMode tmx, SKShaderTileMode tmy, SKFilterMode filterMode, SKMatrix localMatrix, SKRect tile) => ToShader (tmx, tmy, filterMode, &localMatrix, &tile); - private SKShader ToShader (SKShaderTileMode tmx, SKShaderTileMode tmy, SKFilterMode filterMode, SKMatrix* localMatrix, SKRect* tile) => - SKShader.GetObject (SkiaApi.sk_picture_make_shader (Handle, tmx, tmy, filterMode, localMatrix, tile)); + private SKShader ToShader (SKShaderTileMode tmx, SKShaderTileMode tmy, SKFilterMode filterMode, SKMatrix* localMatrix, SKRect* tile) + { + var result = SKShader.GetObject (SkiaApi.sk_picture_make_shader (Handle, tmx, tmy, filterMode, localMatrix, tile)); + return result; + } // Deserialize @@ -121,7 +140,9 @@ public static SKPicture Deserialize (SKData data) if (data == null) throw new ArgumentNullException (nameof (data)); - return GetObject (SkiaApi.sk_picture_deserialize_from_data (data.Handle)); + var picture = GetObject (SkiaApi.sk_picture_deserialize_from_data (data.Handle)); + GC.KeepAlive (data); + return picture; } public static SKPicture Deserialize (Stream stream) @@ -138,7 +159,9 @@ public static SKPicture Deserialize (SKStream stream) if (stream == null) throw new ArgumentNullException (nameof (stream)); - return GetObject (SkiaApi.sk_picture_deserialize_from_stream (stream.Handle)); + var picture = GetObject (SkiaApi.sk_picture_deserialize_from_stream (stream.Handle)); + GC.KeepAlive (stream); + return picture; } // diff --git a/binding/SkiaSharp/SKPictureRecorder.cs b/binding/SkiaSharp/SKPictureRecorder.cs index 6a3cb930737..efe1aa6543a 100644 --- a/binding/SkiaSharp/SKPictureRecorder.cs +++ b/binding/SkiaSharp/SKPictureRecorder.cs @@ -27,7 +27,8 @@ protected override void DisposeNative () => public SKCanvas BeginRecording (SKRect cullRect) { - return OwnedBy (SKCanvas.GetObject (SkiaApi.sk_picture_recorder_begin_recording (Handle, &cullRect), false), this); + var result = OwnedBy (SKCanvas.GetObject (SkiaApi.sk_picture_recorder_begin_recording (Handle, &cullRect), false), this); + return result; } public SKCanvas BeginRecording (SKRect cullRect, bool useRTree) @@ -41,7 +42,8 @@ public SKCanvas BeginRecording (SKRect cullRect, bool useRTree) var rtreeHandle = IntPtr.Zero; try { rtreeHandle = SkiaApi.sk_rtree_factory_new (); - return OwnedBy (SKCanvas.GetObject (SkiaApi.sk_picture_recorder_begin_recording_with_bbh_factory (Handle, &cullRect, rtreeHandle), false), this); + var result = OwnedBy (SKCanvas.GetObject (SkiaApi.sk_picture_recorder_begin_recording_with_bbh_factory (Handle, &cullRect, rtreeHandle), false), this); + return result; } finally { if (rtreeHandle != IntPtr.Zero) { SkiaApi.sk_rtree_factory_delete (rtreeHandle); @@ -51,15 +53,21 @@ public SKCanvas BeginRecording (SKRect cullRect, bool useRTree) public SKPicture EndRecording () { - return SKPicture.GetObject (SkiaApi.sk_picture_recorder_end_recording (Handle)); + var result = SKPicture.GetObject (SkiaApi.sk_picture_recorder_end_recording (Handle)); + return result; } public SKDrawable EndRecordingAsDrawable () { - return SKDrawable.GetObject (SkiaApi.sk_picture_recorder_end_recording_as_drawable (Handle)); + var result = SKDrawable.GetObject (SkiaApi.sk_picture_recorder_end_recording_as_drawable (Handle)); + return result; } - public SKCanvas RecordingCanvas => - OwnedBy (SKCanvas.GetObject (SkiaApi.sk_picture_get_recording_canvas (Handle), false), this); + public SKCanvas RecordingCanvas { + get { + var result = OwnedBy (SKCanvas.GetObject (SkiaApi.sk_picture_get_recording_canvas (Handle), false), this); + return result; + } + } } } diff --git a/binding/SkiaSharp/SKPixmap.cs b/binding/SkiaSharp/SKPixmap.cs index 7ec88b841ec..65b35f475c6 100644 --- a/binding/SkiaSharp/SKPixmap.cs +++ b/binding/SkiaSharp/SKPixmap.cs @@ -93,14 +93,23 @@ public SKSizeI Size { public SKAlphaType AlphaType => Info.AlphaType; - public SKColorSpace? ColorSpace => - SKColorSpace.GetObject (SkiaApi.sk_pixmap_get_colorspace (Handle)); + public SKColorSpace? ColorSpace { + get { + var result = SKColorSpace.GetObject (SkiaApi.sk_pixmap_get_colorspace (Handle)); + return result; + } + } public int BytesPerPixel => Info.BytesPerPixel; public int BitShiftPerPixel => Info.BitShiftPerPixel; - public int RowBytes => (int)SkiaApi.sk_pixmap_get_row_bytes (Handle); + public int RowBytes { + get { + var result = (int)SkiaApi.sk_pixmap_get_row_bytes (Handle); + return result; + } + } public int BytesSize => Info.BytesSize; @@ -108,11 +117,17 @@ public SKSizeI Size { // pixels - public IntPtr GetPixels () => - (IntPtr)SkiaApi.sk_pixmap_get_writable_addr (Handle); + public IntPtr GetPixels () + { + var result = (IntPtr)SkiaApi.sk_pixmap_get_writable_addr (Handle); + return result; + } - public IntPtr GetPixels (int x, int y) => - (IntPtr)SkiaApi.sk_pixmap_get_writeable_addr_with_xy (Handle, x, y); + public IntPtr GetPixels (int x, int y) + { + var result = (IntPtr)SkiaApi.sk_pixmap_get_writeable_addr_with_xy (Handle, x, y); + return result; + } public Span GetPixelSpan () => GetPixelSpan (0, 0); @@ -171,8 +186,11 @@ public unsafe Span GetPixelSpan (int x, int y) return span; } - public SKColor GetPixelColor (int x, int y) => - SkiaApi.sk_pixmap_get_pixel_color (Handle, x, y); + public SKColor GetPixelColor (int x, int y) + { + var result = SkiaApi.sk_pixmap_get_pixel_color (Handle, x, y); + return result; + } public SKColorF GetPixelColorF (int x, int y) { @@ -181,8 +199,11 @@ public SKColorF GetPixelColorF (int x, int y) return color; } - public float GetPixelAlpha (int x, int y) => - SkiaApi.sk_pixmap_get_pixel_alphaf (Handle, x, y); + public float GetPixelAlpha (int x, int y) + { + var result = SkiaApi.sk_pixmap_get_pixel_alphaf (Handle, x, y); + return result; + } // ScalePixels @@ -196,7 +217,9 @@ public bool ScalePixels (SKPixmap destination) => public bool ScalePixels (SKPixmap destination, SKSamplingOptions sampling) { _ = destination ?? throw new ArgumentNullException (nameof (destination)); - return SkiaApi.sk_pixmap_scale_pixels (Handle, destination.Handle, &sampling); + var result = SkiaApi.sk_pixmap_scale_pixels (Handle, destination.Handle, &sampling); + GC.KeepAlive (destination); + return result; } // ReadPixels @@ -204,7 +227,8 @@ public bool ScalePixels (SKPixmap destination, SKSamplingOptions sampling) public bool ReadPixels (SKImageInfo dstInfo, IntPtr dstPixels, int dstRowBytes, int srcX, int srcY) { var cinfo = SKImageInfoNative.FromManaged (ref dstInfo); - return SkiaApi.sk_pixmap_read_pixels (Handle, &cinfo, (void*)dstPixels, (IntPtr)dstRowBytes, srcX, srcY); + var result = SkiaApi.sk_pixmap_read_pixels (Handle, &cinfo, (void*)dstPixels, (IntPtr)dstRowBytes, srcX, srcY); + return result; } public bool ReadPixels (SKImageInfo dstInfo, IntPtr dstPixels, int dstRowBytes) => @@ -264,7 +288,9 @@ public bool Encode (Stream dst, SKWebpEncoderOptions options) public bool Encode (SKWStream dst, SKWebpEncoderOptions options) { _ = dst ?? throw new ArgumentNullException (nameof (dst)); - return SkiaApi.sk_webpencoder_encode (dst.Handle, Handle, &options); + var result = SkiaApi.sk_webpencoder_encode (dst.Handle, Handle, &options); + GC.KeepAlive (dst); + return result; } // Encode (jpeg) @@ -286,7 +312,9 @@ public bool Encode (Stream dst, SKJpegEncoderOptions options) public bool Encode (SKWStream dst, SKJpegEncoderOptions options) { _ = dst ?? throw new ArgumentNullException (nameof (dst)); - return SkiaApi.sk_jpegencoder_encode (dst.Handle, Handle, &options); + var result = SkiaApi.sk_jpegencoder_encode (dst.Handle, Handle, &options); + GC.KeepAlive (dst); + return result; } // Encode (png) @@ -308,7 +336,9 @@ public bool Encode (Stream dst, SKPngEncoderOptions options) public bool Encode (SKWStream dst, SKPngEncoderOptions options) { _ = dst ?? throw new ArgumentNullException (nameof (dst)); - return SkiaApi.sk_pngencoder_encode (dst.Handle, Handle, &options); + var result = SkiaApi.sk_pngencoder_encode (dst.Handle, Handle, &options); + GC.KeepAlive (dst); + return result; } // ExtractSubset @@ -326,7 +356,9 @@ public bool Encode (SKWStream dst, SKPngEncoderOptions options) public bool ExtractSubset (SKPixmap result, SKRectI subset) { _ = result ?? throw new ArgumentNullException (nameof (result)); - return SkiaApi.sk_pixmap_extract_subset (Handle, result.Handle, &subset); + var extracted = SkiaApi.sk_pixmap_extract_subset (Handle, result.Handle, &subset); + GC.KeepAlive (result); + return extracted; } // Erase @@ -334,19 +366,28 @@ public bool ExtractSubset (SKPixmap result, SKRectI subset) public bool Erase (SKColor color) => Erase (color, Rect); - public bool Erase (SKColor color, SKRectI subset) => - SkiaApi.sk_pixmap_erase_color (Handle, (uint)color, &subset); + public bool Erase (SKColor color, SKRectI subset) + { + var result = SkiaApi.sk_pixmap_erase_color (Handle, (uint)color, &subset); + return result; + } public bool Erase (SKColorF color) => Erase (color, Rect); - public bool Erase (SKColorF color, SKRectI subset) => - SkiaApi.sk_pixmap_erase_color4f (Handle, &color, &subset); + public bool Erase (SKColorF color, SKRectI subset) + { + var result = SkiaApi.sk_pixmap_erase_color4f (Handle, &color, &subset); + return result; + } // ComputeIsOpaque - public bool ComputeIsOpaque () => - SkiaApi.sk_pixmap_compute_is_opaque (Handle); + public bool ComputeIsOpaque () + { + var result = SkiaApi.sk_pixmap_compute_is_opaque (Handle); + return result; + } // With* diff --git a/binding/SkiaSharp/SKRegion.cs b/binding/SkiaSharp/SKRegion.cs index dfb835583e8..32f4cbed98c 100644 --- a/binding/SkiaSharp/SKRegion.cs +++ b/binding/SkiaSharp/SKRegion.cs @@ -42,14 +42,26 @@ protected override void DisposeNative () => // properties - public bool IsEmpty => - SkiaApi.sk_region_is_empty (Handle); + public bool IsEmpty { + get { + var result = SkiaApi.sk_region_is_empty (Handle); + return result; + } + } - public bool IsRect => - SkiaApi.sk_region_is_rect (Handle); + public bool IsRect { + get { + var result = SkiaApi.sk_region_is_rect (Handle); + return result; + } + } - public bool IsComplex => - SkiaApi.sk_region_is_complex (Handle); + public bool IsComplex { + get { + var result = SkiaApi.sk_region_is_complex (Handle); + return result; + } + } public SKRectI Bounds { get { @@ -87,34 +99,53 @@ public bool Contains (SKRegion src) if (src == null) throw new ArgumentNullException (nameof (src)); - return SkiaApi.sk_region_contains (Handle, src.Handle); + var result = SkiaApi.sk_region_contains (Handle, src.Handle); + GC.KeepAlive (src); + return result; } - public bool Contains (SKPointI xy) => - SkiaApi.sk_region_contains_point (Handle, xy.X, xy.Y); + public bool Contains (SKPointI xy) + { + var result = SkiaApi.sk_region_contains_point (Handle, xy.X, xy.Y); + return result; + } - public bool Contains (int x, int y) => - SkiaApi.sk_region_contains_point (Handle, x, y); + public bool Contains (int x, int y) + { + var result = SkiaApi.sk_region_contains_point (Handle, x, y); + return result; + } - public bool Contains (SKRectI rect) => - SkiaApi.sk_region_contains_rect (Handle, &rect); + public bool Contains (SKRectI rect) + { + var result = SkiaApi.sk_region_contains_rect (Handle, &rect); + return result; + } // QuickContains - public bool QuickContains (SKRectI rect) => - SkiaApi.sk_region_quick_contains (Handle, &rect); + public bool QuickContains (SKRectI rect) + { + var result = SkiaApi.sk_region_quick_contains (Handle, &rect); + return result; + } // QuickReject - public bool QuickReject (SKRectI rect) => - SkiaApi.sk_region_quick_reject_rect (Handle, &rect); + public bool QuickReject (SKRectI rect) + { + var result = SkiaApi.sk_region_quick_reject_rect (Handle, &rect); + return result; + } public bool QuickReject (SKRegion region) { if (region == null) throw new ArgumentNullException (nameof (region)); - return SkiaApi.sk_region_quick_reject (Handle, region.Handle); + var result = SkiaApi.sk_region_quick_reject (Handle, region.Handle); + GC.KeepAlive (region); + return result; } public bool QuickReject (SKPath path) @@ -142,32 +173,45 @@ public bool Intersects (SKRegion region) if (region == null) throw new ArgumentNullException (nameof (region)); - return SkiaApi.sk_region_intersects (Handle, region.Handle); + var result = SkiaApi.sk_region_intersects (Handle, region.Handle); + GC.KeepAlive (region); + return result; } - public bool Intersects (SKRectI rect) => - SkiaApi.sk_region_intersects_rect (Handle, &rect); + public bool Intersects (SKRectI rect) + { + var result = SkiaApi.sk_region_intersects_rect (Handle, &rect); + return result; + } // Set* - public void SetEmpty () => + public void SetEmpty () + { SkiaApi.sk_region_set_empty (Handle); + } public bool SetRegion (SKRegion region) { if (region == null) throw new ArgumentNullException (nameof (region)); - return SkiaApi.sk_region_set_region (Handle, region.Handle); + var result = SkiaApi.sk_region_set_region (Handle, region.Handle); + GC.KeepAlive (region); + return result; } - public bool SetRect (SKRectI rect) => - SkiaApi.sk_region_set_rect (Handle, &rect); + public bool SetRect (SKRectI rect) + { + var result = SkiaApi.sk_region_set_rect (Handle, &rect); + return result; + } public bool SetRects (ReadOnlySpan rects) { fixed (SKRectI* r = rects) { - return SkiaApi.sk_region_set_rects (Handle, r, rects.Length); + var result = SkiaApi.sk_region_set_rects (Handle, r, rects.Length); + return result; } } @@ -178,7 +222,10 @@ public bool SetPath (SKPath path, SKRegion clip) if (clip == null) throw new ArgumentNullException (nameof (clip)); - return SkiaApi.sk_region_set_path (Handle, path.Handle, clip.Handle); + var result = SkiaApi.sk_region_set_path (Handle, path.Handle, clip.Handle); + GC.KeepAlive (path); + GC.KeepAlive (clip); + return result; } public bool SetPath (SKPath path) @@ -191,24 +238,35 @@ public bool SetPath (SKPath path) if (!rect.IsEmpty) clip.SetRect (rect); - return SkiaApi.sk_region_set_path (Handle, path.Handle, clip.Handle); + var result = SkiaApi.sk_region_set_path (Handle, path.Handle, clip.Handle); + GC.KeepAlive (path); + return result; } // Translate - public void Translate (int x, int y) => + public void Translate (int x, int y) + { SkiaApi.sk_region_translate (Handle, x, y); + } // Op - public bool Op (SKRectI rect, SKRegionOperation op) => - SkiaApi.sk_region_op_rect (Handle, &rect, op); + public bool Op (SKRectI rect, SKRegionOperation op) + { + var result = SkiaApi.sk_region_op_rect (Handle, &rect, op); + return result; + } public bool Op (int left, int top, int right, int bottom, SKRegionOperation op) => Op (new SKRectI (left, top, right, bottom), op); - public bool Op (SKRegion region, SKRegionOperation op) => - SkiaApi.sk_region_op (Handle, region.Handle, op); + public bool Op (SKRegion region, SKRegionOperation op) + { + var result = SkiaApi.sk_region_op (Handle, region.Handle, op); + GC.KeepAlive (region); + return result; + } public bool Op (SKPath path, SKRegionOperation op) { diff --git a/binding/SkiaSharp/SKRoundRect.cs b/binding/SkiaSharp/SKRoundRect.cs index 67f8794da4f..f1e302ce90a 100644 --- a/binding/SkiaSharp/SKRoundRect.cs +++ b/binding/SkiaSharp/SKRoundRect.cs @@ -46,6 +46,7 @@ public SKRoundRect (SKRect rect, float xRadius, float yRadius) public SKRoundRect (SKRoundRect rrect) : this (SkiaApi.sk_rrect_new_copy (rrect.Handle), true) { + GC.KeepAlive (rrect); } protected override void Dispose (bool disposing) => @@ -69,13 +70,33 @@ public SKRect Rect { GetRadii(SKRoundRectCorner.LowerLeft), }; - public SKRoundRectType Type => SkiaApi.sk_rrect_get_type (Handle); + public SKRoundRectType Type { + get { + var r = SkiaApi.sk_rrect_get_type (Handle); + return r; + } + } - public float Width => SkiaApi.sk_rrect_get_width (Handle); + public float Width { + get { + var r = SkiaApi.sk_rrect_get_width (Handle); + return r; + } + } - public float Height => SkiaApi.sk_rrect_get_height (Handle); + public float Height { + get { + var r = SkiaApi.sk_rrect_get_height (Handle); + return r; + } + } - public bool IsValid => SkiaApi.sk_rrect_is_valid (Handle); + public bool IsValid { + get { + var r = SkiaApi.sk_rrect_is_valid (Handle); + return r; + } + } public bool AllCornersCircular => CheckAllCornersCircular (Utils.NearlyZero); @@ -138,7 +159,8 @@ public void SetRectRadii (SKRect rect, ReadOnlySpan radii) public bool Contains (SKRect rect) { - return SkiaApi.sk_rrect_contains (Handle, &rect); + var r = SkiaApi.sk_rrect_contains (Handle, &rect); + return r; } public SKPoint GetRadii (SKRoundRectCorner corner) @@ -181,7 +203,8 @@ public void Offset (float dx, float dy) public bool TryTransform (SKMatrix matrix, out SKRoundRect transformed) { var destHandle = SkiaApi.sk_rrect_new (); - if (SkiaApi.sk_rrect_transform (Handle, &matrix, destHandle)) { + var success = SkiaApi.sk_rrect_transform (Handle, &matrix, destHandle); + if (success) { transformed = new SKRoundRect (destHandle, true); return true; } diff --git a/binding/SkiaSharp/SKRuntimeEffect.cs b/binding/SkiaSharp/SKRuntimeEffect.cs index 9a265379387..84df6d7a8d8 100644 --- a/binding/SkiaSharp/SKRuntimeEffect.cs +++ b/binding/SkiaSharp/SKRuntimeEffect.cs @@ -87,8 +87,12 @@ private static void ValidateResult (SKRuntimeEffect effect, string errors) // properties - public int UniformSize => - (int)SkiaApi.sk_runtimeeffect_get_uniform_byte_size (Handle); + public int UniformSize { + get { + var size = (int)SkiaApi.sk_runtimeeffect_get_uniform_byte_size (Handle); + return size; + } + } public IReadOnlyList Children => children ??= GetChildrenNames ().ToArray (); @@ -138,7 +142,9 @@ private SKShader ToShader (SKData uniforms, SKObject[] children, SKMatrix* local using var childrenHandles = Utils.RentHandlesArray (children, true); fixed (IntPtr* ch = childrenHandles) { - return SKShader.GetObject (SkiaApi.sk_runtimeeffect_make_shader (Handle, uniformsHandle, ch, (IntPtr)childrenHandles.Length, localMatrix)); + var shader = SKShader.GetObject (SkiaApi.sk_runtimeeffect_make_shader (Handle, uniformsHandle, ch, (IntPtr)childrenHandles.Length, localMatrix)); + GC.KeepAlive (uniforms); + return shader; } } @@ -162,7 +168,9 @@ private SKColorFilter ToColorFilter (SKData uniforms, SKObject[] children) using var childrenHandles = Utils.RentHandlesArray (children, true); fixed (IntPtr* ch = childrenHandles) { - return SKColorFilter.GetObject (SkiaApi.sk_runtimeeffect_make_color_filter (Handle, uniformsHandle, ch, (IntPtr)childrenHandles.Length)); + var colorFilter = SKColorFilter.GetObject (SkiaApi.sk_runtimeeffect_make_color_filter (Handle, uniformsHandle, ch, (IntPtr)childrenHandles.Length)); + GC.KeepAlive (uniforms); + return colorFilter; } } @@ -186,7 +194,9 @@ private SKBlender ToBlender (SKData uniforms, SKObject[] children) using var childrenHandles = Utils.RentHandlesArray (children, true); fixed (IntPtr* ch = childrenHandles) { - return SKBlender.GetObject (SkiaApi.sk_runtimeeffect_make_blender (Handle, uniformsHandle, ch, (IntPtr)childrenHandles.Length)); + var blender = SKBlender.GetObject (SkiaApi.sk_runtimeeffect_make_blender (Handle, uniformsHandle, ch, (IntPtr)childrenHandles.Length)); + GC.KeepAlive (uniforms); + return blender; } } diff --git a/binding/SkiaSharp/SKShader.cs b/binding/SkiaSharp/SKShader.cs index 93b676c0bc6..cb88d58e1e5 100644 --- a/binding/SkiaSharp/SKShader.cs +++ b/binding/SkiaSharp/SKShader.cs @@ -22,13 +22,18 @@ public SKShader WithColorFilter (SKColorFilter filter) if (filter == null) throw new ArgumentNullException (nameof (filter)); - return GetObject (SkiaApi.sk_shader_with_color_filter (Handle, filter.Handle)); + var shader = GetObject (SkiaApi.sk_shader_with_color_filter (Handle, filter.Handle)); + GC.KeepAlive (filter); + return shader; } // WithLocalMatrix - public SKShader WithLocalMatrix (SKMatrix localMatrix) => - GetObject (SkiaApi.sk_shader_with_local_matrix (Handle, &localMatrix)); + public SKShader WithLocalMatrix (SKMatrix localMatrix) + { + var shader = GetObject (SkiaApi.sk_shader_with_local_matrix (Handle, &localMatrix)); + return shader; + } // CreateEmpty @@ -45,7 +50,9 @@ public static SKShader CreateColor (SKColorF color, SKColorSpace colorspace) if (colorspace == null) throw new ArgumentNullException (nameof (colorspace)); - return GetObject (SkiaApi.sk_shader_new_color4f (&color, colorspace.Handle)); + var shader = GetObject (SkiaApi.sk_shader_new_color4f (&color, colorspace.Handle)); + GC.KeepAlive (colorspace); + return shader; } // CreateBitmap @@ -163,7 +170,9 @@ public static SKShader CreateLinearGradient (SKPoint start, SKPoint end, SKColor var points = stackalloc SKPoint[] { start, end }; fixed (SKColorF* c = colors) fixed (float* cp = colorPos) { - return GetObject (SkiaApi.sk_shader_new_linear_gradient_color4f (points, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, mode, null)); + var shader = GetObject (SkiaApi.sk_shader_new_linear_gradient_color4f (points, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, mode, null)); + GC.KeepAlive (colorspace); + return shader; } } @@ -177,7 +186,9 @@ public static SKShader CreateLinearGradient (SKPoint start, SKPoint end, SKColor var points = stackalloc SKPoint[] { start, end }; fixed (SKColorF* c = colors) fixed (float* cp = colorPos) { - return GetObject (SkiaApi.sk_shader_new_linear_gradient_color4f (points, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, mode, &localMatrix)); + var shader = GetObject (SkiaApi.sk_shader_new_linear_gradient_color4f (points, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, mode, &localMatrix)); + GC.KeepAlive (colorspace); + return shader; } } @@ -224,7 +235,9 @@ public static SKShader CreateRadialGradient (SKPoint center, float radius, SKCol fixed (SKColorF* c = colors) fixed (float* cp = colorPos) { - return GetObject (SkiaApi.sk_shader_new_radial_gradient_color4f (¢er, radius, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, mode, null)); + var shader = GetObject (SkiaApi.sk_shader_new_radial_gradient_color4f (¢er, radius, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, mode, null)); + GC.KeepAlive (colorspace); + return shader; } } @@ -237,7 +250,9 @@ public static SKShader CreateRadialGradient (SKPoint center, float radius, SKCol fixed (SKColorF* c = colors) fixed (float* cp = colorPos) { - return GetObject (SkiaApi.sk_shader_new_radial_gradient_color4f (¢er, radius, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, mode, &localMatrix)); + var shader = GetObject (SkiaApi.sk_shader_new_radial_gradient_color4f (¢er, radius, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, mode, &localMatrix)); + GC.KeepAlive (colorspace); + return shader; } } @@ -302,7 +317,9 @@ public static SKShader CreateSweepGradient (SKPoint center, SKColorF[] colors, S fixed (SKColorF* c = colors) fixed (float* cp = colorPos) { - return GetObject (SkiaApi.sk_shader_new_sweep_gradient_color4f (¢er, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, tileMode, startAngle, endAngle, null)); + var shader = GetObject (SkiaApi.sk_shader_new_sweep_gradient_color4f (¢er, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, tileMode, startAngle, endAngle, null)); + GC.KeepAlive (colorspace); + return shader; } } @@ -315,7 +332,9 @@ public static SKShader CreateSweepGradient (SKPoint center, SKColorF[] colors, S fixed (SKColorF* c = colors) fixed (float* cp = colorPos) { - return GetObject (SkiaApi.sk_shader_new_sweep_gradient_color4f (¢er, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, tileMode, startAngle, endAngle, &localMatrix)); + var shader = GetObject (SkiaApi.sk_shader_new_sweep_gradient_color4f (¢er, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, tileMode, startAngle, endAngle, &localMatrix)); + GC.KeepAlive (colorspace); + return shader; } } @@ -362,7 +381,9 @@ public static SKShader CreateTwoPointConicalGradient (SKPoint start, float start fixed (SKColorF* c = colors) fixed (float* cp = colorPos) { - return GetObject (SkiaApi.sk_shader_new_two_point_conical_gradient_color4f (&start, startRadius, &end, endRadius, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, mode, null)); + var shader = GetObject (SkiaApi.sk_shader_new_two_point_conical_gradient_color4f (&start, startRadius, &end, endRadius, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, mode, null)); + GC.KeepAlive (colorspace); + return shader; } } @@ -375,7 +396,9 @@ public static SKShader CreateTwoPointConicalGradient (SKPoint start, float start fixed (SKColorF* c = colors) fixed (float* cp = colorPos) { - return GetObject (SkiaApi.sk_shader_new_two_point_conical_gradient_color4f (&start, startRadius, &end, endRadius, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, mode, &localMatrix)); + var shader = GetObject (SkiaApi.sk_shader_new_two_point_conical_gradient_color4f (&start, startRadius, &end, endRadius, c, colorspace?.Handle ?? IntPtr.Zero, cp, colors.Length, mode, &localMatrix)); + GC.KeepAlive (colorspace); + return shader; } } @@ -413,7 +436,10 @@ public static SKShader CreateCompose (SKShader shaderA, SKShader shaderB, SKBlen if (shaderB == null) throw new ArgumentNullException (nameof (shaderB)); - return GetObject (SkiaApi.sk_shader_new_blend (mode, shaderA.Handle, shaderB.Handle)); + var shader = GetObject (SkiaApi.sk_shader_new_blend (mode, shaderA.Handle, shaderB.Handle)); + GC.KeepAlive (shaderA); + GC.KeepAlive (shaderB); + return shader; } // CreateBlend @@ -422,7 +448,10 @@ public static SKShader CreateBlend (SKBlendMode mode, SKShader shaderA, SKShader { _ = shaderA ?? throw new ArgumentNullException (nameof (shaderA)); _ = shaderB ?? throw new ArgumentNullException (nameof (shaderB)); - return GetObject (SkiaApi.sk_shader_new_blend (mode, shaderA.Handle, shaderB.Handle)); + var shader = GetObject (SkiaApi.sk_shader_new_blend (mode, shaderA.Handle, shaderB.Handle)); + GC.KeepAlive (shaderA); + GC.KeepAlive (shaderB); + return shader; } public static SKShader CreateBlend (SKBlender blender, SKShader shaderA, SKShader shaderB) @@ -430,7 +459,11 @@ public static SKShader CreateBlend (SKBlender blender, SKShader shaderA, SKShade _ = shaderA ?? throw new ArgumentNullException (nameof (shaderA)); _ = shaderB ?? throw new ArgumentNullException (nameof (shaderB)); _ = blender ?? throw new ArgumentNullException (nameof (blender)); - return GetObject (SkiaApi.sk_shader_new_blender (blender.Handle, shaderA.Handle, shaderB.Handle)); + var shader = GetObject (SkiaApi.sk_shader_new_blender (blender.Handle, shaderA.Handle, shaderB.Handle)); + GC.KeepAlive (blender); + GC.KeepAlive (shaderA); + GC.KeepAlive (shaderB); + return shader; } // CreateColorFilter diff --git a/binding/SkiaSharp/SKStream.cs b/binding/SkiaSharp/SKStream.cs index ac16bacddd5..6fedca71833 100644 --- a/binding/SkiaSharp/SKStream.cs +++ b/binding/SkiaSharp/SKStream.cs @@ -14,7 +14,8 @@ internal SKStream (IntPtr handle, bool owns) public bool IsAtEnd { get { - return SkiaApi.sk_stream_is_at_end (Handle); + var result = SkiaApi.sk_stream_is_at_end (Handle); + return result; } } @@ -70,42 +71,48 @@ public bool ReadBool () public bool ReadSByte (out SByte buffer) { fixed (SByte* b = &buffer) { - return SkiaApi.sk_stream_read_s8 (Handle, b); + var result = SkiaApi.sk_stream_read_s8 (Handle, b); + return result; } } public bool ReadInt16 (out Int16 buffer) { fixed (Int16* b = &buffer) { - return SkiaApi.sk_stream_read_s16 (Handle, b); + var result = SkiaApi.sk_stream_read_s16 (Handle, b); + return result; } } public bool ReadInt32 (out Int32 buffer) { fixed (Int32* b = &buffer) { - return SkiaApi.sk_stream_read_s32 (Handle, b); + var result = SkiaApi.sk_stream_read_s32 (Handle, b); + return result; } } public bool ReadByte (out Byte buffer) { fixed (Byte* b = &buffer) { - return SkiaApi.sk_stream_read_u8 (Handle, b); + var result = SkiaApi.sk_stream_read_u8 (Handle, b); + return result; } } public bool ReadUInt16 (out UInt16 buffer) { fixed (UInt16* b = &buffer) { - return SkiaApi.sk_stream_read_u16 (Handle, b); + var result = SkiaApi.sk_stream_read_u16 (Handle, b); + return result; } } public bool ReadUInt32 (out UInt32 buffer) { fixed (UInt32* b = &buffer) { - return SkiaApi.sk_stream_read_u32 (Handle, b); + var result = SkiaApi.sk_stream_read_u32 (Handle, b); + return result; } } @@ -126,27 +133,32 @@ public int Read (byte[] buffer, int size) public int Read (IntPtr buffer, int size) { - return (int)SkiaApi.sk_stream_read (Handle, (void*)buffer, (IntPtr)size); + var result = (int)SkiaApi.sk_stream_read (Handle, (void*)buffer, (IntPtr)size); + return result; } public int Peek (IntPtr buffer, int size) { - return (int)SkiaApi.sk_stream_peek (Handle, (void*)buffer, (IntPtr)size); + var result = (int)SkiaApi.sk_stream_peek (Handle, (void*)buffer, (IntPtr)size); + return result; } public int Skip (int size) { - return (int)SkiaApi.sk_stream_skip (Handle, (IntPtr)size); + var result = (int)SkiaApi.sk_stream_skip (Handle, (IntPtr)size); + return result; } public bool Rewind () { - return SkiaApi.sk_stream_rewind (Handle); + var result = SkiaApi.sk_stream_rewind (Handle); + return result; } public bool Seek (int position) { - return SkiaApi.sk_stream_seek (Handle, (IntPtr)position); + var result = SkiaApi.sk_stream_seek (Handle, (IntPtr)position); + return result; } [Obsolete ("The native stream move offset is capped at a 32-bit int. Use Move(int) instead.")] @@ -154,47 +166,62 @@ public bool Seek (int position) public bool Move (int offset) { - return SkiaApi.sk_stream_move (Handle, offset); + var result = SkiaApi.sk_stream_move (Handle, offset); + return result; } public IntPtr GetMemoryBase () { - return (IntPtr)SkiaApi.sk_stream_get_memory_base (Handle); + var result = (IntPtr)SkiaApi.sk_stream_get_memory_base (Handle); + return result; } public SKData GetData () { - return SKData.GetObject (SkiaApi.sk_stream_get_data (Handle)); + var result = SKData.GetObject (SkiaApi.sk_stream_get_data (Handle)); + return result; } - internal SKStream Fork () => GetObject (SkiaApi.sk_stream_fork (Handle)); + internal SKStream Fork () + { + var result = GetObject (SkiaApi.sk_stream_fork (Handle)); + return result; + } - internal SKStream Duplicate () => GetObject (SkiaApi.sk_stream_duplicate (Handle)); + internal SKStream Duplicate () + { + var result = GetObject (SkiaApi.sk_stream_duplicate (Handle)); + return result; + } public bool HasPosition { get { - return SkiaApi.sk_stream_has_position (Handle); + var result = SkiaApi.sk_stream_has_position (Handle); + return result; } } public int Position { get { - return (int)SkiaApi.sk_stream_get_position (Handle); + var result = (int)SkiaApi.sk_stream_get_position (Handle); + return result; } - set { + set { Seek (value); } } public bool HasLength { get { - return SkiaApi.sk_stream_has_length (Handle); + var result = SkiaApi.sk_stream_has_length (Handle); + return result; } } public int Length { get { - return (int)SkiaApi.sk_stream_get_length (Handle); + var result = (int)SkiaApi.sk_stream_get_length (Handle); + return result; } } @@ -294,7 +321,12 @@ protected override void Dispose (bool disposing) => protected override void DisposeNative () => SkiaApi.sk_filestream_destroy (Handle); - public bool IsValid => SkiaApi.sk_filestream_is_valid (Handle); + public bool IsValid { + get { + var result = SkiaApi.sk_filestream_is_valid (Handle); + return result; + } + } public static bool IsPathSupported (string path) => true; @@ -387,20 +419,23 @@ internal SKWStream (IntPtr handle, bool owns) public virtual int BytesWritten { get { - return (int)SkiaApi.sk_wstream_bytes_written (Handle); + var result = (int)SkiaApi.sk_wstream_bytes_written (Handle); + return result; } } public virtual bool Write (byte[] buffer, int size) { fixed (byte* b = buffer) { - return SkiaApi.sk_wstream_write (Handle, (void*)b, (IntPtr)size); + var result = SkiaApi.sk_wstream_write (Handle, (void*)b, (IntPtr)size); + return result; } } public bool NewLine () { - return SkiaApi.sk_wstream_newline (Handle); + var result = SkiaApi.sk_wstream_newline (Handle); + return result; } public virtual void Flush () @@ -410,57 +445,68 @@ public virtual void Flush () public bool Write8 (Byte value) { - return SkiaApi.sk_wstream_write_8 (Handle, value); + var result = SkiaApi.sk_wstream_write_8 (Handle, value); + return result; } public bool Write16 (UInt16 value) { - return SkiaApi.sk_wstream_write_16 (Handle, value); + var result = SkiaApi.sk_wstream_write_16 (Handle, value); + return result; } public bool Write32 (UInt32 value) { - return SkiaApi.sk_wstream_write_32 (Handle, value); + var result = SkiaApi.sk_wstream_write_32 (Handle, value); + return result; } public bool WriteText (string value) { - return SkiaApi.sk_wstream_write_text (Handle, value); + var result = SkiaApi.sk_wstream_write_text (Handle, value); + return result; } public bool WriteDecimalAsTest (Int32 value) { - return SkiaApi.sk_wstream_write_dec_as_text (Handle, value); + var result = SkiaApi.sk_wstream_write_dec_as_text (Handle, value); + return result; } public bool WriteBigDecimalAsText (Int64 value, int digits) { - return SkiaApi.sk_wstream_write_bigdec_as_text (Handle, value, digits); + var result = SkiaApi.sk_wstream_write_bigdec_as_text (Handle, value, digits); + return result; } public bool WriteHexAsText (UInt32 value, int digits) { - return SkiaApi.sk_wstream_write_hex_as_text (Handle, value, digits); + var result = SkiaApi.sk_wstream_write_hex_as_text (Handle, value, digits); + return result; } public bool WriteScalarAsText (float value) { - return SkiaApi.sk_wstream_write_scalar_as_text (Handle, value); + var result = SkiaApi.sk_wstream_write_scalar_as_text (Handle, value); + return result; } public bool WriteBool (bool value) { - return SkiaApi.sk_wstream_write_bool (Handle, value); + var result = SkiaApi.sk_wstream_write_bool (Handle, value); + return result; } public bool WriteScalar (float value) { - return SkiaApi.sk_wstream_write_scalar (Handle, value); + var result = SkiaApi.sk_wstream_write_scalar (Handle, value); + return result; } public bool WritePackedUInt32 (UInt32 value) { - return SkiaApi.sk_wstream_write_packed_uint (Handle, (IntPtr)value); + var result = SkiaApi.sk_wstream_write_packed_uint (Handle, (IntPtr)value); + return result; } public bool WriteStream (SKStream input, int length) @@ -469,7 +515,9 @@ public bool WriteStream (SKStream input, int length) throw new ArgumentNullException (nameof(input)); } - return SkiaApi.sk_wstream_write_stream (Handle, input.Handle, (IntPtr)length); + var result = SkiaApi.sk_wstream_write_stream (Handle, input.Handle, (IntPtr)length); + GC.KeepAlive (input); + return result; } public static int GetSizeOfPackedUInt32 (UInt32 value) @@ -507,7 +555,12 @@ protected override void Dispose (bool disposing) => protected override void DisposeNative () => SkiaApi.sk_filewstream_destroy (Handle); - public bool IsValid => SkiaApi.sk_filewstream_is_valid (Handle); + public bool IsValid { + get { + var result = SkiaApi.sk_filewstream_is_valid (Handle); + return result; + } + } public static bool IsPathSupported (string path) => true; @@ -546,12 +599,14 @@ public SKData CopyToData () public SKStreamAsset DetachAsStream () { - return SKStreamAssetImplementation.GetObject (SkiaApi.sk_dynamicmemorywstream_detach_as_stream (Handle)); + var result = SKStreamAssetImplementation.GetObject (SkiaApi.sk_dynamicmemorywstream_detach_as_stream (Handle)); + return result; } public SKData DetachAsData () { - return SKData.GetObject (SkiaApi.sk_dynamicmemorywstream_detach_as_data (Handle)); + var result = SKData.GetObject (SkiaApi.sk_dynamicmemorywstream_detach_as_data (Handle)); + return result; } public void CopyTo (IntPtr data) @@ -574,7 +629,9 @@ public bool CopyTo (SKWStream dst) { if (dst == null) throw new ArgumentNullException (nameof (dst)); - return SkiaApi.sk_dynamicmemorywstream_write_to_stream (Handle, dst.Handle); + var result = SkiaApi.sk_dynamicmemorywstream_write_to_stream (Handle, dst.Handle); + GC.KeepAlive (dst); + return result; } public bool CopyTo (Stream dst) diff --git a/binding/SkiaSharp/SKString.cs b/binding/SkiaSharp/SKString.cs index b48a84f23f0..79d30e025eb 100644 --- a/binding/SkiaSharp/SKString.cs +++ b/binding/SkiaSharp/SKString.cs @@ -49,7 +49,8 @@ public override string ToString () { var cstr = SkiaApi.sk_string_get_c_str (Handle); var clen = SkiaApi.sk_string_get_size (Handle); - return StringUtilities.GetString ((IntPtr)cstr, (int)clen, SKTextEncoding.Utf8); + var result = StringUtilities.GetString ((IntPtr)cstr, (int)clen, SKTextEncoding.Utf8); + return result; } public static explicit operator string (SKString skString) diff --git a/binding/SkiaSharp/SKSurface.cs b/binding/SkiaSharp/SKSurface.cs index ba7fc03f796..96945c6a162 100644 --- a/binding/SkiaSharp/SKSurface.cs +++ b/binding/SkiaSharp/SKSurface.cs @@ -29,7 +29,9 @@ public static SKSurface Create (SKImageInfo info, SKSurfaceProperties props) => public static SKSurface Create (SKImageInfo info, int rowBytes, SKSurfaceProperties props) { var cinfo = SKImageInfoNative.FromManaged (ref info); - return GetObject (SkiaApi.sk_surface_new_raster (&cinfo, (IntPtr)rowBytes, props?.Handle ?? IntPtr.Zero)); + var surface = GetObject (SkiaApi.sk_surface_new_raster (&cinfo, (IntPtr)rowBytes, props?.Handle ?? IntPtr.Zero)); + GC.KeepAlive (props); + return surface; } // convenience RASTER DIRECT to use a SKPixmap instead of SKImageInfo and IntPtr @@ -70,7 +72,9 @@ public static SKSurface Create (SKImageInfo info, IntPtr pixels, int rowBytes, S : releaseProc; DelegateProxies.Create (del, out _, out var ctx); var proxy = del != null ? DelegateProxies.SKSurfaceRasterReleaseProxy : null; - return GetObject (SkiaApi.sk_surface_new_raster_direct (&cinfo, (void*)pixels, (IntPtr)rowBytes, proxy, (void*)ctx, props?.Handle ?? IntPtr.Zero)); + var surface = GetObject (SkiaApi.sk_surface_new_raster_direct (&cinfo, (void*)pixels, (IntPtr)rowBytes, proxy, (void*)ctx, props?.Handle ?? IntPtr.Zero)); + GC.KeepAlive (props); + return surface; } // GPU BACKEND RENDER TARGET surface @@ -115,7 +119,12 @@ public static SKSurface Create (GRRecordingContext context, GRBackendRenderTarge if (renderTarget == null) throw new ArgumentNullException (nameof (renderTarget)); - return GetObject (SkiaApi.sk_surface_new_backend_render_target (context.Handle, renderTarget.Handle, origin, colorType.ToNative (), colorspace?.Handle ?? IntPtr.Zero, props?.Handle ?? IntPtr.Zero)); + var surface = GetObject (SkiaApi.sk_surface_new_backend_render_target (context.Handle, renderTarget.Handle, origin, colorType.ToNative (), colorspace?.Handle ?? IntPtr.Zero, props?.Handle ?? IntPtr.Zero)); + GC.KeepAlive (context); + GC.KeepAlive (renderTarget); + GC.KeepAlive (colorspace); + GC.KeepAlive (props); + return surface; } // GPU BACKEND TEXTURE surface @@ -172,7 +181,12 @@ public static SKSurface Create (GRRecordingContext context, GRBackendTexture tex if (texture == null) throw new ArgumentNullException (nameof (texture)); - return GetObject (SkiaApi.sk_surface_new_backend_texture (context.Handle, texture.Handle, origin, sampleCount, colorType.ToNative (), colorspace?.Handle ?? IntPtr.Zero, props?.Handle ?? IntPtr.Zero)); + var surface = GetObject (SkiaApi.sk_surface_new_backend_texture (context.Handle, texture.Handle, origin, sampleCount, colorType.ToNative (), colorspace?.Handle ?? IntPtr.Zero, props?.Handle ?? IntPtr.Zero)); + GC.KeepAlive (context); + GC.KeepAlive (texture); + GC.KeepAlive (colorspace); + GC.KeepAlive (props); + return surface; } // GPU NEW surface @@ -216,7 +230,10 @@ public static SKSurface Create (GRRecordingContext context, bool budgeted, SKIma throw new ArgumentNullException (nameof (context)); var cinfo = SKImageInfoNative.FromManaged (ref info); - return GetObject (SkiaApi.sk_surface_new_render_target (context.Handle, budgeted, &cinfo, sampleCount, origin, props?.Handle ?? IntPtr.Zero, shouldCreateWithMips)); + var surface = GetObject (SkiaApi.sk_surface_new_render_target (context.Handle, budgeted, &cinfo, sampleCount, origin, props?.Handle ?? IntPtr.Zero, shouldCreateWithMips)); + GC.KeepAlive (context); + GC.KeepAlive (props); + return surface; } #if __MACOS__ || __IOS__ || __TVOS__ @@ -240,6 +257,9 @@ public static SKSurface Create (GRRecordingContext context, CoreAnimation.CAMeta { void* drawablePtr; var surface = GetObject (SkiaApi.sk_surface_new_metal_layer (context.Handle, (void*)(IntPtr)layer.Handle, origin, sampleCount, colorType.ToNative (), colorspace?.Handle ?? IntPtr.Zero, props?.Handle ?? IntPtr.Zero, &drawablePtr)); + GC.KeepAlive (context); + GC.KeepAlive (colorspace); + GC.KeepAlive (props); drawable = ObjCRuntime.Runtime.GetINativeObject ((IntPtr)drawablePtr, true); return surface; } @@ -250,8 +270,14 @@ public static SKSurface Create (GRRecordingContext context, MetalKit.MTKView vie public static SKSurface Create (GRRecordingContext context, MetalKit.MTKView view, GRSurfaceOrigin origin, int sampleCount, SKColorType colorType, SKColorSpace colorspace) => Create (context, view, origin, sampleCount, colorType, colorspace, null); - public static SKSurface Create (GRRecordingContext context, MetalKit.MTKView view, GRSurfaceOrigin origin, int sampleCount, SKColorType colorType, SKColorSpace colorspace, SKSurfaceProperties props) => - GetObject (SkiaApi.sk_surface_new_metal_view (context.Handle, (void*)(IntPtr)view.Handle, origin, sampleCount, colorType.ToNative (), colorspace?.Handle ?? IntPtr.Zero, props?.Handle ?? IntPtr.Zero)); + public static SKSurface Create (GRRecordingContext context, MetalKit.MTKView view, GRSurfaceOrigin origin, int sampleCount, SKColorType colorType, SKColorSpace colorspace, SKSurfaceProperties props) + { + var surface = GetObject (SkiaApi.sk_surface_new_metal_view (context.Handle, (void*)(IntPtr)view.Handle, origin, sampleCount, colorType.ToNative (), colorspace?.Handle ?? IntPtr.Zero, props?.Handle ?? IntPtr.Zero)); + GC.KeepAlive (context); + GC.KeepAlive (colorspace); + GC.KeepAlive (props); + return surface; + } #endif @@ -262,20 +288,38 @@ public static SKSurface CreateNull (int width, int height) => // - public SKCanvas Canvas => - OwnedBy (SKCanvas.GetObject (SkiaApi.sk_surface_get_canvas (Handle), false, unrefExisting: false), this); + public SKCanvas Canvas { + get { + var result = OwnedBy (SKCanvas.GetObject (SkiaApi.sk_surface_get_canvas (Handle), false, unrefExisting: false), this); + return result; + } + } - public SKSurfaceProperties SurfaceProperties => - OwnedBy (SKSurfaceProperties.GetObject (SkiaApi.sk_surface_get_props (Handle), false), this); + public SKSurfaceProperties SurfaceProperties { + get { + var result = OwnedBy (SKSurfaceProperties.GetObject (SkiaApi.sk_surface_get_props (Handle), false), this); + return result; + } + } - public GRRecordingContext Context => - GRRecordingContext.GetObject (SkiaApi.sk_surface_get_recording_context (Handle), false, unrefExisting: false); + public GRRecordingContext Context { + get { + var result = GRRecordingContext.GetObject (SkiaApi.sk_surface_get_recording_context (Handle), false, unrefExisting: false); + return result; + } + } - public SKImage Snapshot () => - SKImage.GetObject (SkiaApi.sk_surface_new_image_snapshot (Handle)); + public SKImage Snapshot () + { + var result = SKImage.GetObject (SkiaApi.sk_surface_new_image_snapshot (Handle)); + return result; + } - public SKImage Snapshot (SKRectI bounds) => - SKImage.GetObject (SkiaApi.sk_surface_new_image_snapshot_with_crop (Handle, &bounds)); + public SKImage Snapshot (SKRectI bounds) + { + var result = SKImage.GetObject (SkiaApi.sk_surface_new_image_snapshot_with_crop (Handle, &bounds)); + return result; + } public void Draw (SKCanvas canvas, float x, float y, SKPaint paint) { @@ -283,6 +327,8 @@ public void Draw (SKCanvas canvas, float x, float y, SKPaint paint) throw new ArgumentNullException (nameof (canvas)); SkiaApi.sk_surface_draw (Handle, canvas.Handle, x, y, paint == null ? IntPtr.Zero : paint.Handle); + GC.KeepAlive (canvas); + GC.KeepAlive (paint); } public void Draw (SKCanvas canvas, SKPoint p, SKSamplingOptions sampling, SKPaint paint = null) @@ -296,6 +342,8 @@ public void Draw (SKCanvas canvas, float x, float y, SKSamplingOptions sampling, throw new ArgumentNullException (nameof (canvas)); SkiaApi.sk_surface_draw_with_sampling (Handle, canvas.Handle, x, y, &sampling, paint == null ? IntPtr.Zero : paint.Handle); + GC.KeepAlive (canvas); + GC.KeepAlive (paint); } public SKPixmap PeekPixels () @@ -316,6 +364,7 @@ public bool PeekPixels (SKPixmap pixmap) throw new ArgumentNullException (nameof (pixmap)); var result = SkiaApi.sk_surface_peek_pixels (Handle, pixmap.Handle); + GC.KeepAlive (pixmap); if (result) pixmap.pixelSource = this; return result; @@ -325,7 +374,6 @@ public bool ReadPixels (SKImageInfo dstInfo, IntPtr dstPixels, int dstRowBytes, { var cinfo = SKImageInfoNative.FromManaged (ref dstInfo); var result = SkiaApi.sk_surface_read_pixels (Handle, &cinfo, (void*)dstPixels, (IntPtr)dstRowBytes, srcX, srcY); - GC.KeepAlive (this); return result; } diff --git a/binding/SkiaSharp/SKTextBlob.cs b/binding/SkiaSharp/SKTextBlob.cs index 10193d25a0c..a9016c1cea2 100644 --- a/binding/SkiaSharp/SKTextBlob.cs +++ b/binding/SkiaSharp/SKTextBlob.cs @@ -12,9 +12,15 @@ internal SKTextBlob (IntPtr x, bool owns) protected override void Dispose (bool disposing) => base.Dispose (disposing); - void ISKNonVirtualReferenceCounted.ReferenceNative () => SkiaApi.sk_textblob_ref (Handle); + void ISKNonVirtualReferenceCounted.ReferenceNative () + { + SkiaApi.sk_textblob_ref (Handle); + } - void ISKNonVirtualReferenceCounted.UnreferenceNative () => SkiaApi.sk_textblob_unref (Handle); + void ISKNonVirtualReferenceCounted.UnreferenceNative () + { + SkiaApi.sk_textblob_unref (Handle); + } public SKRect Bounds { get { @@ -24,7 +30,12 @@ public SKRect Bounds { } } - public uint UniqueId => SkiaApi.sk_textblob_get_unique_id (Handle); + public uint UniqueId { + get { + var r = SkiaApi.sk_textblob_get_unique_id (Handle); + return r; + } + } // Create @@ -240,6 +251,7 @@ public void GetIntercepts (float upperBounds, float lowerBounds, Span int bounds[1] = lowerBounds; fixed (float* i = intervals) { SkiaApi.sk_textblob_get_intercepts (Handle, bounds, i, paint?.Handle ?? IntPtr.Zero); + GC.KeepAlive (paint); } } @@ -250,7 +262,9 @@ public int CountIntercepts (float upperBounds, float lowerBounds, SKPaint? paint var bounds = stackalloc float[2]; bounds[0] = upperBounds; bounds[1] = lowerBounds; - return SkiaApi.sk_textblob_get_intercepts (Handle, bounds, null, paint?.Handle ?? IntPtr.Zero); + var result = SkiaApi.sk_textblob_get_intercepts (Handle, bounds, null, paint?.Handle ?? IntPtr.Zero); + GC.KeepAlive (paint); + return result; } // @@ -274,15 +288,16 @@ public SKTextBlobBuilder () protected override void Dispose (bool disposing) => base.Dispose (disposing); - protected override void DisposeNative () => + protected override void DisposeNative () + { SkiaApi.sk_textblob_builder_delete (Handle); + } // Build public SKTextBlob? Build () { var blob = SKTextBlob.GetObject (SkiaApi.sk_textblob_builder_make (Handle)); - GC.KeepAlive (this); return blob; } @@ -396,6 +411,7 @@ public SKRawRunBuffer AllocateRawRun (SKFont font, int count, float x, fl else SkiaApi.sk_textblob_builder_alloc_run (Handle, font.Handle, count, x, y, null, &runbuffer); + GC.KeepAlive (font); return new SKRawRunBuffer (runbuffer, count, 0, 0); } @@ -416,6 +432,7 @@ public SKRawRunBuffer AllocateRawTextRun (SKFont font, int count, float x else SkiaApi.sk_textblob_builder_alloc_run_text (Handle, font.Handle, count, x, y, textByteCount, null, &runbuffer); + GC.KeepAlive (font); return new SKRawRunBuffer (runbuffer, count, 0, textByteCount); } @@ -438,6 +455,7 @@ public SKRawRunBuffer AllocateRawHorizontalRun (SKFont font, int count, f else SkiaApi.sk_textblob_builder_alloc_run_pos_h (Handle, font.Handle, count, y, null, &runbuffer); + GC.KeepAlive (font); return new SKRawRunBuffer (runbuffer, count, count, 0); } @@ -458,6 +476,7 @@ public SKRawRunBuffer AllocateRawHorizontalTextRun (SKFont font, int coun else SkiaApi.sk_textblob_builder_alloc_run_text_pos_h (Handle, font.Handle, count, y, textByteCount, null, &runbuffer); + GC.KeepAlive (font); return new SKRawRunBuffer (runbuffer, count, count, textByteCount); } @@ -481,6 +500,7 @@ public SKRawRunBuffer AllocateRawPositionedRun (SKFont font, int count, else SkiaApi.sk_textblob_builder_alloc_run_pos (Handle, font.Handle, count, null, &runbuffer); + GC.KeepAlive (font); return new SKRawRunBuffer (runbuffer, count, count, 0); } @@ -501,6 +521,7 @@ public SKRawRunBuffer AllocateRawPositionedTextRun (SKFont font, int co else SkiaApi.sk_textblob_builder_alloc_run_text_pos (Handle, font.Handle, count, textByteCount, null, &runbuffer); + GC.KeepAlive (font); return new SKRawRunBuffer (runbuffer, count, count, textByteCount); } @@ -523,6 +544,7 @@ public SKRawRunBuffer AllocateRawRotationScaleRun (SKFont else SkiaApi.sk_textblob_builder_alloc_run_rsxform (Handle, font.Handle, count, null, &runbuffer); + GC.KeepAlive (font); return new SKRawRunBuffer (runbuffer, count, count, 0); } @@ -543,6 +565,7 @@ public SKRawRunBuffer AllocateRawRotationScaleTextRun (SK else SkiaApi.sk_textblob_builder_alloc_run_text_rsxform (Handle, font.Handle, count, textByteCount, null, &runbuffer); + GC.KeepAlive (font); return new SKRawRunBuffer (runbuffer, count, count, textByteCount); } } diff --git a/binding/SkiaSharp/SKTypeface.cs b/binding/SkiaSharp/SKTypeface.cs index a84076418af..3b7c15c4f24 100644 --- a/binding/SkiaSharp/SKTypeface.cs +++ b/binding/SkiaSharp/SKTypeface.cs @@ -94,31 +94,81 @@ public static SKTypeface FromData (SKData data, int index = 0) => // Properties - public string FamilyName => (string)SKString.GetObject (SkiaApi.sk_typeface_get_family_name (Handle)); + public string FamilyName { + get { + var r = (string)SKString.GetObject (SkiaApi.sk_typeface_get_family_name (Handle)); + return r; + } + } - public SKFontStyle FontStyle => SKFontStyle.GetObject (SkiaApi.sk_typeface_get_fontstyle (Handle)); + public SKFontStyle FontStyle { + get { + var r = SKFontStyle.GetObject (SkiaApi.sk_typeface_get_fontstyle (Handle)); + return r; + } + } - public int FontWeight => SkiaApi.sk_typeface_get_font_weight (Handle); + public int FontWeight { + get { + var r = SkiaApi.sk_typeface_get_font_weight (Handle); + return r; + } + } - public int FontWidth => SkiaApi.sk_typeface_get_font_width (Handle); + public int FontWidth { + get { + var r = SkiaApi.sk_typeface_get_font_width (Handle); + return r; + } + } - public SKFontStyleSlant FontSlant => SkiaApi.sk_typeface_get_font_slant (Handle); + public SKFontStyleSlant FontSlant { + get { + var r = SkiaApi.sk_typeface_get_font_slant (Handle); + return r; + } + } public bool IsBold => FontStyle.Weight >= (int)SKFontStyleWeight.SemiBold; public bool IsItalic => FontStyle.Slant != SKFontStyleSlant.Upright; - public bool IsFixedPitch => SkiaApi.sk_typeface_is_fixed_pitch (Handle); + public bool IsFixedPitch { + get { + var r = SkiaApi.sk_typeface_is_fixed_pitch (Handle); + return r; + } + } - public int UnitsPerEm => SkiaApi.sk_typeface_get_units_per_em (Handle); + public int UnitsPerEm { + get { + var r = SkiaApi.sk_typeface_get_units_per_em (Handle); + return r; + } + } - public int GlyphCount => SkiaApi.sk_typeface_count_glyphs (Handle); + public int GlyphCount { + get { + var r = SkiaApi.sk_typeface_count_glyphs (Handle); + return r; + } + } - public string PostScriptName => (string)SKString.GetObject (SkiaApi.sk_typeface_get_post_script_name (Handle)); + public string PostScriptName { + get { + var r = (string)SKString.GetObject (SkiaApi.sk_typeface_get_post_script_name (Handle)); + return r; + } + } // GetTableTags - public int TableCount => SkiaApi.sk_typeface_count_tables (Handle); + public int TableCount { + get { + var r = SkiaApi.sk_typeface_count_tables (Handle); + return r; + } + } public UInt32[] GetTableTags () { @@ -143,8 +193,11 @@ public bool TryGetTableTags (out UInt32[] tags) // GetTableSize - public int GetTableSize (UInt32 tag) => - (int)SkiaApi.sk_typeface_get_table_size (Handle, tag); + public int GetTableSize (UInt32 tag) + { + var r = (int)SkiaApi.sk_typeface_get_table_size (Handle, tag); + return r; + } // GetTableData @@ -267,7 +320,8 @@ public SKStreamAsset OpenStream () => public SKStreamAsset OpenStream (out int ttcIndex) { fixed (int* ttc = &ttcIndex) { - return SKStreamAsset.GetObject (SkiaApi.sk_typeface_open_stream (Handle, ttc)); + var r = SKStreamAsset.GetObject (SkiaApi.sk_typeface_open_stream (Handle, ttc)); + return r; } } @@ -277,8 +331,12 @@ public SKStreamAsset OpenStream (out int ttcIndex) /// If false, then will never return nonzero /// adjustments for any possible pair of glyphs. /// - public bool HasGetKerningPairAdjustments => - SkiaApi.sk_typeface_get_kerning_pair_adjustments (Handle, null, 0, null); + public bool HasGetKerningPairAdjustments { + get { + var r = SkiaApi.sk_typeface_get_kerning_pair_adjustments (Handle, null, 0, null); + return r; + } + } /// /// Gets a kerning adjustment for each sequential pair of glyph indices in . @@ -336,8 +394,12 @@ public bool GetKerningPairAdjustments (ReadOnlySpan glyphs, Span ad // Variable fonts - public int VariationDesignParameterCount => - SkiaApi.sk_typeface_get_variation_design_parameters (Handle, null, 0); + public int VariationDesignParameterCount { + get { + var r = SkiaApi.sk_typeface_get_variation_design_parameters (Handle, null, 0); + return r; + } + } public SKFontVariationAxis[] VariationDesignParameters { @@ -361,8 +423,9 @@ public int GetVariationDesignParameters (Span axes) fixed (SKFontVariationAxis* ptr = axes) { var total = SkiaApi.sk_typeface_get_variation_design_parameters (Handle, ptr, axes.Length); - if (total <= axes.Length) + if (total <= axes.Length) { return total; + } // Skia is all-or-nothing: if buffer is undersized it writes nothing. // Retry with a pooled buffer and copy what fits. @@ -375,8 +438,12 @@ public int GetVariationDesignParameters (Span axes) } } - public int VariationDesignPositionCount => - SkiaApi.sk_typeface_get_variation_design_position (Handle, null, 0); + public int VariationDesignPositionCount { + get { + var r = SkiaApi.sk_typeface_get_variation_design_position (Handle, null, 0); + return r; + } + } public SKFontVariationPositionCoordinate[] VariationDesignPosition { @@ -400,8 +467,9 @@ public int GetVariationDesignPosition (Span c fixed (SKFontVariationPositionCoordinate* ptr = coordinates) { var total = SkiaApi.sk_typeface_get_variation_design_position (Handle, ptr, coordinates.Length); - if (total <= coordinates.Length) + if (total <= coordinates.Length) { return total; + } // Skia is all-or-nothing: if buffer is undersized it writes nothing. // Retry with a pooled buffer and copy what fits. @@ -417,7 +485,8 @@ public int GetVariationDesignPosition (Span c public SKTypeface Clone (ReadOnlySpan position) { fixed (SKFontVariationPositionCoordinate* ptr = position) { - return GetObject (SkiaApi.sk_typeface_clone_with_arguments (Handle, ptr, position.Length, 0, 0, null, 0)); + var r = GetObject (SkiaApi.sk_typeface_clone_with_arguments (Handle, ptr, position.Length, 0, 0, null, 0)); + return r; } } @@ -425,14 +494,16 @@ public SKTypeface Clone (int paletteIndex) { if (paletteIndex < 0) throw new ArgumentOutOfRangeException (nameof (paletteIndex)); - return GetObject (SkiaApi.sk_typeface_clone_with_arguments (Handle, null, 0, 0, paletteIndex, null, 0)); + var r = GetObject (SkiaApi.sk_typeface_clone_with_arguments (Handle, null, 0, 0, paletteIndex, null, 0)); + return r; } public SKTypeface Clone (SKFontArguments args) { fixed (SKFontVariationPositionCoordinate* posPtr = args.VariationDesignPosition) fixed (SKFontPaletteOverride* palPtr = args.PaletteOverrides) { - return GetObject (SkiaApi.sk_typeface_clone_with_arguments (Handle, posPtr, args.VariationDesignPosition.Length, args.CollectionIndex, args.PaletteIndex, palPtr, args.PaletteOverrides.Length)); + var r = GetObject (SkiaApi.sk_typeface_clone_with_arguments (Handle, posPtr, args.VariationDesignPosition.Length, args.CollectionIndex, args.PaletteIndex, palPtr, args.PaletteOverrides.Length)); + return r; } } diff --git a/binding/SkiaSharp/SKWebpEncoder.cs b/binding/SkiaSharp/SKWebpEncoder.cs index e848a397414..d520c68bd61 100644 --- a/binding/SkiaSharp/SKWebpEncoder.cs +++ b/binding/SkiaSharp/SKWebpEncoder.cs @@ -12,7 +12,10 @@ public static bool Encode (SKWStream dst, SKPixmap src, SKWebpEncoderOptions opt _ = dst ?? throw new ArgumentNullException (nameof (dst)); _ = src ?? throw new ArgumentNullException (nameof (src)); - return SkiaApi.sk_webpencoder_encode (dst.Handle, src.Handle, &options); + var result = SkiaApi.sk_webpencoder_encode (dst.Handle, src.Handle, &options); + GC.KeepAlive (dst); + GC.KeepAlive (src); + return result; } public static bool Encode (Stream dst, SKPixmap src, SKWebpEncoderOptions options) @@ -49,7 +52,9 @@ public static bool EncodeAnimated (SKWStream dst, ReadOnlySpan use-after-free + // -> the process is killed by an AccessViolation. + // + // This test forces that race with a GC/finalizer hammer thread. On a correct build + // (the ctor keeps `path` alive) it runs to completion and passes. On a regressed build + // it crashes the test host within the time budget below. + // + // It lives in the serialized HandleDictionary threading collection so its GC hammer + // does not perturb tests running in parallel. + [Collection (HandleDictionaryThreadingCollection.Name)] + public class Pr4080UseAfterFreeTest : SKTest + { + // Long enough to land the race on a regressed build, short enough for CI on a fixed one. + private static readonly TimeSpan Budget = TimeSpan.FromSeconds (4); + + [SkippableFact] + public void PathMeasureCtorKeepsPathAliveAcrossNativeCall () + { + var stop = 0; + + // Continuously collect and drain finalizers so any wrapper that becomes unrooted + // mid-native-call is freed immediately, deterministically exposing a missing KeepAlive. + var hammer = new Thread (() => { + while (Volatile.Read (ref stop) == 0) { + GC.Collect (); + GC.WaitForPendingFinalizers (); + } + }) { + IsBackground = true, + Name = "gc-hammer", + }; + hammer.Start (); + + try { + var sw = Stopwatch.StartNew (); + while (sw.Elapsed < Budget) { + // The argument is a temporary with no other root: once its Handle is read + // inside the ctor it is collectible, so the hammer thread can finalize it + // (freeing the native SkPath) while the native ctor is still reading it - + // unless the ctor keeps it alive. + for (var i = 0; i < 200; i++) + (new SKPathMeasure (MakeDoomedPath ())).Dispose (); + } + } finally { + Volatile.Write (ref stop, 1); + hammer.Join (); + } + + // Reaching here means the wrapper survived every native call: no use-after-free. + Assert.True (true); + } + + [MethodImpl (MethodImplOptions.NoInlining)] + private static SKPath MakeDoomedPath () + { + var p = new SKPath (); + + // A non-trivial path makes the native ctor (SkContourMeasureIter::reset -> + // SkPath::isFinite) spend longer reading the path, widening the use-after-free + // window so a regression is caught quickly. + for (var i = 0; i < 1500; i++) + p.LineTo (i, (i & 1) == 0 ? 0 : 10); + + return p; + } + } +} From ddada7058f36eadf9a34cf64fbecd725e4a7054b Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Tue, 9 Jun 2026 10:24:28 -0400 Subject: [PATCH 26/29] Keep 'this' alive across instance P/Invoke calls (1C lifetime hardening) Same use-after-free class as the previous commit, but for the instance receiver: an instance member doing SkiaApi.foo(Handle, ...) can have 'this' garbage-collected and finalized during the native call (after Handle is read), freeing the native object mid-call. Add GC.KeepAlive(this) after the native call in instance members across SkiaSharp/HarfBuzzSharp wrappers. Reduces the net48 in-flight UAF failures from 1-4/run to 0-1/run. (A residual teardown/finalization crash tied to the dispose-protected singletons remains and is tracked separately.) Co-Authored-By: Claude Opus 4.8 --- binding/HarfBuzzSharp/Blob.cs | 6 ++ binding/HarfBuzzSharp/Buffer.cs | 39 +++++++++++++ binding/HarfBuzzSharp/Face.cs | 34 +++++++++++ binding/HarfBuzzSharp/Font.cs | 37 ++++++++++++ binding/HarfBuzzSharp/FontFunctions.cs | 18 ++++++ binding/HarfBuzzSharp/UnicodeFunctions.cs | 14 +++++ binding/SkiaSharp/GRBackendRenderTarget.cs | 7 +++ binding/SkiaSharp/GRBackendTexture.cs | 6 ++ binding/SkiaSharp/GRContext.cs | 15 +++++ binding/SkiaSharp/GRGlInterface.cs | 2 + binding/SkiaSharp/GRRecordingContext.cs | 5 ++ binding/SkiaSharp/SKBitmap.cs | 26 +++++++++ binding/SkiaSharp/SKCanvas.cs | 65 ++++++++++++++++++++++ binding/SkiaSharp/SKCodec.cs | 21 +++++++ binding/SkiaSharp/SKColorSpace.cs | 10 ++++ binding/SkiaSharp/SKData.cs | 7 +++ binding/SkiaSharp/SKDocument.cs | 5 ++ binding/SkiaSharp/SKDrawable.cs | 6 ++ binding/SkiaSharp/SKFont.cs | 40 +++++++++++++ binding/SkiaSharp/SKFontManager.cs | 9 +++ binding/SkiaSharp/SKFontStyleSet.cs | 5 ++ binding/SkiaSharp/SKImage.cs | 23 ++++++++ binding/SkiaSharp/SKNWayCanvas.cs | 3 + binding/SkiaSharp/SKPaint.cs | 46 +++++++++++++++ binding/SkiaSharp/SKPath.cs | 32 +++++++++++ binding/SkiaSharp/SKPathBuilder.cs | 47 ++++++++++++++++ binding/SkiaSharp/SKPathMeasure.cs | 9 +++ binding/SkiaSharp/SKPicture.cs | 8 +++ binding/SkiaSharp/SKPictureRecorder.cs | 5 ++ binding/SkiaSharp/SKPixmap.cs | 20 +++++++ binding/SkiaSharp/SKRegion.cs | 29 ++++++++++ binding/SkiaSharp/SKRoundRect.cs | 17 ++++++ binding/SkiaSharp/SKRuntimeEffect.cs | 6 ++ binding/SkiaSharp/SKShader.cs | 2 + binding/SkiaSharp/SKStream.cs | 47 ++++++++++++++++ binding/SkiaSharp/SKString.cs | 1 + binding/SkiaSharp/SKSurface.cs | 9 +++ binding/SkiaSharp/SKTextBlob.cs | 16 ++++++ binding/SkiaSharp/SKTypeface.cs | 28 ++++++++++ 39 files changed, 725 insertions(+) diff --git a/binding/HarfBuzzSharp/Blob.cs b/binding/HarfBuzzSharp/Blob.cs index 10626b239b8..9f662740325 100644 --- a/binding/HarfBuzzSharp/Blob.cs +++ b/binding/HarfBuzzSharp/Blob.cs @@ -39,6 +39,7 @@ protected override void DisposeHandler () public int Length { get { var r = (int)HarfBuzzApi.hb_blob_get_length (Handle); + GC.KeepAlive (this); return r; } } @@ -46,6 +47,7 @@ public int Length { public int FaceCount { get { var r = (int)HarfBuzzApi.hb_face_count (Handle); + GC.KeepAlive (this); return r; } } @@ -53,6 +55,7 @@ public int FaceCount { public bool IsImmutable { get { var r = HarfBuzzApi.hb_blob_is_immutable (Handle); + GC.KeepAlive (this); return r; } } @@ -60,12 +63,14 @@ public bool IsImmutable { public void MakeImmutable () { HarfBuzzApi.hb_blob_make_immutable (Handle); + GC.KeepAlive (this); } public unsafe Stream AsStream () { uint length; var dataPtr = HarfBuzzApi.hb_blob_get_data (Handle, &length); + GC.KeepAlive (this); return new UnmanagedMemoryStream ((byte*)dataPtr, length); } @@ -73,6 +78,7 @@ public unsafe Span AsSpan () { uint length; var dataPtr = HarfBuzzApi.hb_blob_get_data (Handle, &length); + GC.KeepAlive (this); return new Span (dataPtr, (int)length); } diff --git a/binding/HarfBuzzSharp/Buffer.cs b/binding/HarfBuzzSharp/Buffer.cs index 33055f2acf0..625296a8b8a 100644 --- a/binding/HarfBuzzSharp/Buffer.cs +++ b/binding/HarfBuzzSharp/Buffer.cs @@ -24,108 +24,129 @@ public Buffer () public ContentType ContentType { get { var r = HarfBuzzApi.hb_buffer_get_content_type (Handle); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_buffer_set_content_type (Handle, value); + GC.KeepAlive (this); } } public Direction Direction { get { var r = HarfBuzzApi.hb_buffer_get_direction (Handle); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_buffer_set_direction (Handle, value); + GC.KeepAlive (this); } } public Language Language { get { var r = new Language (HarfBuzzApi.hb_buffer_get_language (Handle)); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_buffer_set_language (Handle, value.Handle); GC.KeepAlive (value); + GC.KeepAlive (this); } } public BufferFlags Flags { get { var r = HarfBuzzApi.hb_buffer_get_flags (Handle); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_buffer_set_flags (Handle, value); + GC.KeepAlive (this); } } public ClusterLevel ClusterLevel { get { var r = HarfBuzzApi.hb_buffer_get_cluster_level (Handle); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_buffer_set_cluster_level (Handle, value); + GC.KeepAlive (this); } } public uint ReplacementCodepoint { get { var r = HarfBuzzApi.hb_buffer_get_replacement_codepoint (Handle); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_buffer_set_replacement_codepoint (Handle, value); + GC.KeepAlive (this); } } public uint InvisibleGlyph { get { var r = HarfBuzzApi.hb_buffer_get_invisible_glyph (Handle); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_buffer_set_invisible_glyph (Handle, value); + GC.KeepAlive (this); } } public Script Script { get { var r = HarfBuzzApi.hb_buffer_get_script (Handle); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_buffer_set_script (Handle, value); + GC.KeepAlive (this); } } public int Length { get { var r = (int)HarfBuzzApi.hb_buffer_get_length (Handle); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_buffer_set_length (Handle, (uint)value); + GC.KeepAlive (this); } } public UnicodeFunctions UnicodeFunctions { get { var r = new UnicodeFunctions (HarfBuzzApi.hb_buffer_get_unicode_funcs (Handle)); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_buffer_set_unicode_funcs (Handle, value.Handle); GC.KeepAlive (value); + GC.KeepAlive (this); } } public GlyphInfo[] GlyphInfos { get { var array = GetGlyphInfoSpan ().ToArray (); + GC.KeepAlive (this); return array; } } @@ -133,6 +154,7 @@ public GlyphInfo[] GlyphInfos { public GlyphPosition[] GlyphPositions { get { var array = GetGlyphPositionSpan ().ToArray (); + GC.KeepAlive (this); return array; } } @@ -147,6 +169,7 @@ public void Add (uint codepoint, uint cluster) throw new InvalidOperationException ("ContentType must not be of type Glyphs"); HarfBuzzApi.hb_buffer_add (Handle, codepoint, cluster); + GC.KeepAlive (this); } public void AddUtf8 (string utf8text) => AddUtf8 (Encoding.UTF8.GetBytes (utf8text), 0, -1); @@ -174,6 +197,7 @@ public void AddUtf8 (IntPtr text, int textLength, int itemOffset, int itemLength throw new InvalidOperationException ("ContentType must not be Glyphs"); HarfBuzzApi.hb_buffer_add_utf8 (Handle, (void*)text, textLength, (uint)itemOffset, itemLength); + GC.KeepAlive (this); } public void AddUtf16 (string text) => AddUtf16 (text, 0, -1); @@ -214,6 +238,7 @@ public void AddUtf16 (IntPtr text, int textLength, int itemOffset, int itemLengt throw new InvalidOperationException ("ContentType must not be of type Glyphs"); HarfBuzzApi.hb_buffer_add_utf16 (Handle, (ushort*)text, textLength, (uint)itemOffset, itemLength); + GC.KeepAlive (this); } public void AddUtf32 (string text) => AddUtf32 (Encoding.UTF32.GetBytes (text)); @@ -256,6 +281,7 @@ public void AddUtf32 (IntPtr text, int textLength, int itemOffset, int itemLengt throw new InvalidOperationException ("ContentType must not be of type Glyphs"); HarfBuzzApi.hb_buffer_add_utf32 (Handle, (uint*)text, textLength, (uint)itemOffset, itemLength); + GC.KeepAlive (this); } public void AddCodepoints (ReadOnlySpan text) => AddCodepoints (text, 0, -1); @@ -288,12 +314,14 @@ public void AddCodepoints (IntPtr text, int textLength, int itemOffset, int item throw new InvalidOperationException ("ContentType must not be of type Glyphs"); HarfBuzzApi.hb_buffer_add_codepoints (Handle, (uint*)text, textLength, (uint)itemOffset, itemLength); + GC.KeepAlive (this); } public unsafe ReadOnlySpan GetGlyphInfoSpan () { uint length; var infoPtrs = HarfBuzzApi.hb_buffer_get_glyph_infos (Handle, &length); + GC.KeepAlive (this); return new ReadOnlySpan (infoPtrs, (int)length); } @@ -301,6 +329,7 @@ public unsafe ReadOnlySpan GetGlyphPositionSpan () { uint length; var infoPtrs = HarfBuzzApi.hb_buffer_get_glyph_positions (Handle, &length); + GC.KeepAlive (this); return new ReadOnlySpan (infoPtrs, (int)length); } @@ -310,16 +339,19 @@ public void GuessSegmentProperties () throw new InvalidOperationException ("ContentType must be of type Unicode."); HarfBuzzApi.hb_buffer_guess_segment_properties (Handle); + GC.KeepAlive (this); } public void ClearContents () { HarfBuzzApi.hb_buffer_clear_contents (Handle); + GC.KeepAlive (this); } public void Reset () { HarfBuzzApi.hb_buffer_reset (Handle); + GC.KeepAlive (this); } public void Append (Buffer buffer) => Append (buffer, 0, -1); @@ -333,6 +365,7 @@ public void Append (Buffer buffer, int start, int end) HarfBuzzApi.hb_buffer_append (Handle, buffer.Handle, (uint)start, (uint)(end == -1 ? buffer.Length : end)); GC.KeepAlive (buffer); + GC.KeepAlive (this); } public void NormalizeGlyphs () @@ -343,21 +376,25 @@ public void NormalizeGlyphs () throw new InvalidOperationException ("GlyphPositions can't be empty."); HarfBuzzApi.hb_buffer_normalize_glyphs (Handle); + GC.KeepAlive (this); } public void Reverse () { HarfBuzzApi.hb_buffer_reverse (Handle); + GC.KeepAlive (this); } public void ReverseRange (int start, int end) { HarfBuzzApi.hb_buffer_reverse_range (Handle, (uint)start, (uint)(end == -1 ? Length : end)); + GC.KeepAlive (this); } public void ReverseClusters () { HarfBuzzApi.hb_buffer_reverse_clusters (Handle); + GC.KeepAlive (this); } public string SerializeGlyphs () => @@ -406,6 +443,7 @@ public unsafe string SerializeGlyphs (int start, int end, Font font, SerializeFo } GC.KeepAlive (font); + GC.KeepAlive (this); return builder.ToString (); } @@ -425,6 +463,7 @@ public void DeserializeGlyphs (string data, Font font, SerializeFormat format) HarfBuzzApi.hb_buffer_deserialize_glyphs (Handle, data, -1, null, font?.Handle ?? IntPtr.Zero, format); GC.KeepAlive (font); + GC.KeepAlive (this); } protected override void Dispose (bool disposing) => diff --git a/binding/HarfBuzzSharp/Face.cs b/binding/HarfBuzzSharp/Face.cs index 2687a80728a..baaf6cb4847 100644 --- a/binding/HarfBuzzSharp/Face.cs +++ b/binding/HarfBuzzSharp/Face.cs @@ -55,30 +55,36 @@ internal Face (IntPtr handle) public int Index { get { var r = (int)HarfBuzzApi.hb_face_get_index (Handle); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_face_set_index (Handle, (uint)value); + GC.KeepAlive (this); } } public int UnitsPerEm { get { var r = (int)HarfBuzzApi.hb_face_get_upem (Handle); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_face_set_upem (Handle, (uint)value); + GC.KeepAlive (this); } } public int GlyphCount { get { var r = (int)HarfBuzzApi.hb_face_get_glyph_count (Handle); + GC.KeepAlive (this); return r; } set { HarfBuzzApi.hb_face_set_glyph_count (Handle, (uint)value); + GC.KeepAlive (this); } } @@ -90,6 +96,7 @@ public unsafe Tag[] Tables { fixed (void* ptr = buffer) { HarfBuzzApi.hb_face_get_table_tags (Handle, 0, &count, (uint*)ptr); } + GC.KeepAlive (this); return buffer; } } @@ -97,12 +104,14 @@ public unsafe Tag[] Tables { public Blob ReferenceTable (Tag table) { var r = new Blob (HarfBuzzApi.hb_face_reference_table (Handle, table)); + GC.KeepAlive (this); return r; } public bool IsImmutable { get { var r = HarfBuzzApi.hb_face_is_immutable (Handle); + GC.KeepAlive (this); return r; } } @@ -110,6 +119,7 @@ public bool IsImmutable { public void MakeImmutable () { HarfBuzzApi.hb_face_make_immutable (Handle); + GC.KeepAlive (this); } // Variable font support @@ -117,6 +127,7 @@ public void MakeImmutable () public bool HasVariationData { get { var r = HarfBuzzApi.hb_ot_var_has_data (Handle); + GC.KeepAlive (this); return r; } } @@ -124,6 +135,7 @@ public bool HasVariationData { public int VariationAxisCount { get { var r = (int)HarfBuzzApi.hb_ot_var_get_axis_count (Handle); + GC.KeepAlive (this); return r; } } @@ -133,6 +145,7 @@ public OpenTypeVarAxisInfo[] VariationAxisInfos get { var count = HarfBuzzApi.hb_ot_var_get_axis_count (Handle); if (count == 0) { + GC.KeepAlive (this); return Array.Empty (); } @@ -140,6 +153,7 @@ public OpenTypeVarAxisInfo[] VariationAxisInfos fixed (OpenTypeVarAxisInfo* ptr = axes) { HarfBuzzApi.hb_ot_var_get_axis_infos (Handle, 0, &count, ptr); } + GC.KeepAlive (this); return axes; } } @@ -150,6 +164,7 @@ public int GetVariationAxisInfos (Span axes) fixed (OpenTypeVarAxisInfo* ptr = axes) { HarfBuzzApi.hb_ot_var_get_axis_infos (Handle, 0, &count, ptr); } + GC.KeepAlive (this); return (int)count; } @@ -158,6 +173,7 @@ public bool TryFindVariationAxis (Tag tag, out OpenTypeVarAxisInfo axisInfo) axisInfo = default; fixed (OpenTypeVarAxisInfo* ptr = &axisInfo) { var r = HarfBuzzApi.hb_ot_var_find_axis_info (Handle, tag, ptr); + GC.KeepAlive (this); return r; } } @@ -165,6 +181,7 @@ public bool TryFindVariationAxis (Tag tag, out OpenTypeVarAxisInfo axisInfo) public int NamedInstanceCount { get { var r = (int)HarfBuzzApi.hb_ot_var_get_named_instance_count (Handle); + GC.KeepAlive (this); return r; } } @@ -174,6 +191,7 @@ public OpenTypeNameId GetNamedInstanceSubfamilyNameId (int instanceIndex) if (instanceIndex < 0) throw new ArgumentOutOfRangeException (nameof (instanceIndex)); var r = HarfBuzzApi.hb_ot_var_named_instance_get_subfamily_name_id (Handle, (uint)instanceIndex); + GC.KeepAlive (this); return r; } @@ -182,6 +200,7 @@ public OpenTypeNameId GetNamedInstancePostScriptNameId (int instanceIndex) if (instanceIndex < 0) throw new ArgumentOutOfRangeException (nameof (instanceIndex)); var r = HarfBuzzApi.hb_ot_var_named_instance_get_postscript_name_id (Handle, (uint)instanceIndex); + GC.KeepAlive (this); return r; } @@ -192,6 +211,7 @@ public int GetNamedInstanceDesignCoordsCount (int instanceIndex) // Return value is the total number of design coordinates var r = (int)HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, null, null); + GC.KeepAlive (this); return r; } @@ -203,6 +223,7 @@ public float[] GetNamedInstanceDesignCoords (int instanceIndex) // Return value is the total number of design coordinates var totalCoords = (int)HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, null, null); if (totalCoords == 0) { + GC.KeepAlive (this); return Array.Empty (); } @@ -211,6 +232,7 @@ public float[] GetNamedInstanceDesignCoords (int instanceIndex) fixed (float* ptr = coords) { HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, &coordsLength, ptr); } + GC.KeepAlive (this); return coords; } @@ -223,6 +245,7 @@ public int GetNamedInstanceDesignCoords (int instanceIndex, Span coords) fixed (float* ptr = coords) { HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, &coordsLength, ptr); } + GC.KeepAlive (this); return (int)coordsLength; } @@ -231,6 +254,7 @@ public int GetNamedInstanceDesignCoords (int instanceIndex, Span coords) public bool HasPalettes { get { var r = HarfBuzzApi.hb_ot_color_has_palettes (Handle); + GC.KeepAlive (this); return r; } } @@ -238,6 +262,7 @@ public bool HasPalettes { public int PaletteCount { get { var r = (int)HarfBuzzApi.hb_ot_color_palette_get_count (Handle); + GC.KeepAlive (this); return r; } } @@ -249,6 +274,7 @@ public HBColor[] GetPaletteColors (int paletteIndex) var totalColors = (int)HarfBuzzApi.hb_ot_color_palette_get_colors (Handle, (uint)paletteIndex, 0, null, null); if (totalColors == 0) { + GC.KeepAlive (this); return Array.Empty (); } @@ -257,6 +283,7 @@ public HBColor[] GetPaletteColors (int paletteIndex) fixed (HBColor* ptr = colors) { HarfBuzzApi.hb_ot_color_palette_get_colors (Handle, (uint)paletteIndex, 0, &count, ptr); } + GC.KeepAlive (this); return colors; } @@ -269,6 +296,7 @@ public int GetPaletteColors (int paletteIndex, Span colors) fixed (HBColor* ptr = colors) { HarfBuzzApi.hb_ot_color_palette_get_colors (Handle, (uint)paletteIndex, 0, &count, ptr); } + GC.KeepAlive (this); return (int)count; } @@ -277,6 +305,7 @@ public OpenTypeColorPaletteFlags GetPaletteFlags (int paletteIndex) if (paletteIndex < 0) throw new ArgumentOutOfRangeException (nameof (paletteIndex)); var r = HarfBuzzApi.hb_ot_color_palette_get_flags (Handle, (uint)paletteIndex); + GC.KeepAlive (this); return r; } @@ -285,6 +314,7 @@ public OpenTypeNameId GetPaletteNameId (int paletteIndex) if (paletteIndex < 0) throw new ArgumentOutOfRangeException (nameof (paletteIndex)); var r = HarfBuzzApi.hb_ot_color_palette_get_name_id (Handle, (uint)paletteIndex); + GC.KeepAlive (this); return r; } @@ -293,12 +323,14 @@ public OpenTypeNameId GetPaletteColorNameId (int colorIndex) if (colorIndex < 0) throw new ArgumentOutOfRangeException (nameof (colorIndex)); var r = HarfBuzzApi.hb_ot_color_palette_color_get_name_id (Handle, (uint)colorIndex); + GC.KeepAlive (this); return r; } public bool HasColorLayers { get { var r = HarfBuzzApi.hb_ot_color_has_layers (Handle); + GC.KeepAlive (this); return r; } } @@ -306,6 +338,7 @@ public bool HasColorLayers { public bool HasColorPng { get { var r = HarfBuzzApi.hb_ot_color_has_png (Handle); + GC.KeepAlive (this); return r; } } @@ -313,6 +346,7 @@ public bool HasColorPng { public bool HasColorSvg { get { var r = HarfBuzzApi.hb_ot_color_has_svg (Handle); + GC.KeepAlive (this); return r; } } diff --git a/binding/HarfBuzzSharp/Font.cs b/binding/HarfBuzzSharp/Font.cs index 926a466e875..52915563a4d 100644 --- a/binding/HarfBuzzSharp/Font.cs +++ b/binding/HarfBuzzSharp/Font.cs @@ -57,6 +57,7 @@ public void SetFontFunctions (FontFunctions fontFunctions, object fontData, Rele var ctx = DelegateProxies.CreateMultiUserData (destroy, container); HarfBuzzApi.hb_font_set_funcs (Handle, fontFunctions.Handle, (void*)ctx, DelegateProxies.DestroyProxyForMulti); GC.KeepAlive (fontFunctions); + GC.KeepAlive (this); } public void GetScale (out int xScale, out int yScale) @@ -65,17 +66,20 @@ public void GetScale (out int xScale, out int yScale) fixed (int* y = &yScale) { HarfBuzzApi.hb_font_get_scale (Handle, x, y); } + GC.KeepAlive (this); } public void SetScale (int xScale, int yScale) { HarfBuzzApi.hb_font_set_scale (Handle, xScale, yScale); + GC.KeepAlive (this); } public bool TryGetHorizontalFontExtents (out FontExtents extents) { fixed (FontExtents* e = &extents) { var r = HarfBuzzApi.hb_font_get_h_extents (Handle, e); + GC.KeepAlive (this); return r; } } @@ -84,6 +88,7 @@ public bool TryGetVerticalFontExtents (out FontExtents extents) { fixed (FontExtents* e = &extents) { var r = HarfBuzzApi.hb_font_get_v_extents (Handle, e); + GC.KeepAlive (this); return r; } } @@ -95,6 +100,7 @@ public bool TryGetNominalGlyph (uint unicode, out uint glyph) { fixed (uint* g = &glyph) { var r = HarfBuzzApi.hb_font_get_nominal_glyph (Handle, unicode, g); + GC.KeepAlive (this); return r; } } @@ -106,6 +112,7 @@ public bool TryGetVariationGlyph (uint unicode, out uint glyph) { fixed (uint* g = &glyph) { var r = HarfBuzzApi.hb_font_get_variation_glyph (Handle, unicode, 0, g); + GC.KeepAlive (this); return r; } } @@ -117,6 +124,7 @@ public bool TryGetVariationGlyph (uint unicode, uint variationSelector, out uint { fixed (uint* g = &glyph) { var r = HarfBuzzApi.hb_font_get_variation_glyph (Handle, unicode, variationSelector, g); + GC.KeepAlive (this); return r; } } @@ -124,12 +132,14 @@ public bool TryGetVariationGlyph (uint unicode, uint variationSelector, out uint public int GetHorizontalGlyphAdvance (uint glyph) { var r = HarfBuzzApi.hb_font_get_glyph_h_advance (Handle, glyph); + GC.KeepAlive (this); return r; } public int GetVerticalGlyphAdvance (uint glyph) { var r = HarfBuzzApi.hb_font_get_glyph_v_advance (Handle, glyph); + GC.KeepAlive (this); return r; } @@ -148,6 +158,7 @@ public unsafe int[] GetHorizontalGlyphAdvances (IntPtr firstGlyph, int count) HarfBuzzApi.hb_font_get_glyph_h_advances (Handle, (uint)count, (uint*)firstGlyph, 4, firstAdvance, 4); } + GC.KeepAlive (this); return advances; } @@ -166,6 +177,7 @@ public unsafe int[] GetVerticalGlyphAdvances (IntPtr firstGlyph, int count) HarfBuzzApi.hb_font_get_glyph_v_advances (Handle, (uint)count, (uint*)firstGlyph, 4, firstAdvance, 4); } + GC.KeepAlive (this); return advances; } @@ -174,6 +186,7 @@ public bool TryGetHorizontalGlyphOrigin (uint glyph, out int xOrigin, out int yO fixed (int* x = &xOrigin) fixed (int* y = &yOrigin) { var r = HarfBuzzApi.hb_font_get_glyph_h_origin (Handle, glyph, x, y); + GC.KeepAlive (this); return r; } } @@ -183,6 +196,7 @@ public bool TryGetVerticalGlyphOrigin (uint glyph, out int xOrigin, out int yOri fixed (int* x = &xOrigin) fixed (int* y = &yOrigin) { var r = HarfBuzzApi.hb_font_get_glyph_v_origin (Handle, glyph, x, y); + GC.KeepAlive (this); return r; } } @@ -190,6 +204,7 @@ public bool TryGetVerticalGlyphOrigin (uint glyph, out int xOrigin, out int yOri public int GetHorizontalGlyphKerning (uint leftGlyph, uint rightGlyph) { var r = HarfBuzzApi.hb_font_get_glyph_h_kerning (Handle, leftGlyph, rightGlyph); + GC.KeepAlive (this); return r; } @@ -197,6 +212,7 @@ public bool TryGetGlyphExtents (uint glyph, out GlyphExtents extents) { fixed (GlyphExtents* e = &extents) { var r = HarfBuzzApi.hb_font_get_glyph_extents (Handle, glyph, e); + GC.KeepAlive (this); return r; } } @@ -206,6 +222,7 @@ public bool TryGetGlyphContourPoint (uint glyph, uint pointIndex, out int x, out fixed (int* xPtr = &x) fixed (int* yPtr = &y) { var r = HarfBuzzApi.hb_font_get_glyph_contour_point (Handle, glyph, pointIndex, xPtr, yPtr); + GC.KeepAlive (this); return r; } } @@ -217,9 +234,11 @@ public unsafe bool TryGetGlyphName (uint glyph, out string name) try { fixed (byte* first = buffer) { if (!HarfBuzzApi.hb_font_get_glyph_name (Handle, glyph, first, (uint)buffer.Length)) { + GC.KeepAlive (this); name = string.Empty; return false; } + GC.KeepAlive (this); name = Marshal.PtrToStringAnsi ((IntPtr)first); return true; } @@ -232,6 +251,7 @@ public bool TryGetGlyphFromName (string name, out uint glyph) { fixed (uint* g = &glyph) { var r = HarfBuzzApi.hb_font_get_glyph_from_name (Handle, name, name.Length, g); + GC.KeepAlive (this); return r; } } @@ -249,6 +269,7 @@ public bool TryGetGlyph (uint unicode, uint variationSelector, out uint glyph) { fixed (uint* g = &glyph) { var r = HarfBuzzApi.hb_font_get_glyph (Handle, unicode, variationSelector, g); + GC.KeepAlive (this); return r; } } @@ -257,6 +278,7 @@ public FontExtents GetFontExtentsForDirection (Direction direction) { FontExtents extents; HarfBuzzApi.hb_font_get_extents_for_direction (Handle, direction, &extents); + GC.KeepAlive (this); return extents; } @@ -266,6 +288,7 @@ public void GetGlyphAdvanceForDirection (uint glyph, Direction direction, out in fixed (int* yPtr = &y) { HarfBuzzApi.hb_font_get_glyph_advance_for_direction (Handle, glyph, direction, xPtr, yPtr); } + GC.KeepAlive (this); } public unsafe int[] GetGlyphAdvancesForDirection (ReadOnlySpan glyphs, Direction direction) @@ -283,6 +306,7 @@ public unsafe int[] GetGlyphAdvancesForDirection (IntPtr firstGlyph, int count, HarfBuzzApi.hb_font_get_glyph_advances_for_direction (Handle, direction, (uint)count, (uint*)firstGlyph, 4, firstAdvance, 4); } + GC.KeepAlive (this); return advances; } @@ -291,6 +315,7 @@ public bool TryGetGlyphContourPointForOrigin (uint glyph, uint pointIndex, Direc fixed (int* xPtr = &x) fixed (int* yPtr = &y) { var r = HarfBuzzApi.hb_font_get_glyph_contour_point_for_origin (Handle, glyph, pointIndex, direction, xPtr, yPtr); + GC.KeepAlive (this); return r; } } @@ -302,6 +327,7 @@ public unsafe string GlyphToString (uint glyph) try { fixed (byte* first = buffer) { HarfBuzzApi.hb_font_glyph_to_string (Handle, glyph, first, (uint)buffer.Length); + GC.KeepAlive (this); return Marshal.PtrToStringAnsi ((IntPtr)first); } } finally { @@ -313,6 +339,7 @@ public bool TryGetGlyphFromString (string s, out uint glyph) { fixed (uint* g = &glyph) { var r = HarfBuzzApi.hb_font_glyph_from_string (Handle, s, -1, g); + GC.KeepAlive (this); return r; } } @@ -324,6 +351,7 @@ public void SetVariations (ReadOnlySpan variations) fixed (Variation* ptr = variations) { HarfBuzzApi.hb_font_set_variations (Handle, ptr, (uint)variations.Length); } + GC.KeepAlive (this); } public void SetVariationCoordsDesign (ReadOnlySpan coords) @@ -331,6 +359,7 @@ public void SetVariationCoordsDesign (ReadOnlySpan coords) fixed (float* ptr = coords) { HarfBuzzApi.hb_font_set_var_coords_design (Handle, ptr, (uint)coords.Length); } + GC.KeepAlive (this); } public void SetVariationCoordsNormalized (ReadOnlySpan coords) @@ -338,6 +367,7 @@ public void SetVariationCoordsNormalized (ReadOnlySpan coords) fixed (int* ptr = coords) { HarfBuzzApi.hb_font_set_var_coords_normalized (Handle, ptr, (uint)coords.Length); } + GC.KeepAlive (this); } public int[] VariationCoordsNormalized @@ -346,6 +376,7 @@ public int[] VariationCoordsNormalized uint length; var ptr = HarfBuzzApi.hb_font_get_var_coords_normalized (Handle, &length); if (length == 0 || ptr == null) { + GC.KeepAlive (this); return Array.Empty (); } @@ -353,6 +384,7 @@ public int[] VariationCoordsNormalized var coords = new int[count]; for (int i = 0; i < count; i++) coords[i] = ptr[i]; + GC.KeepAlive (this); return coords; } } @@ -362,12 +394,14 @@ public int GetVariationCoordsNormalized (Span coords) uint length; var ptr = HarfBuzzApi.hb_font_get_var_coords_normalized (Handle, &length); if (length == 0 || ptr == null) { + GC.KeepAlive (this); return 0; } var count = Math.Min ((int)length, coords.Length); for (int i = 0; i < count; i++) coords[i] = ptr[i]; + GC.KeepAlive (this); return count; } @@ -376,11 +410,13 @@ public void SetVariationNamedInstance (int instanceIndex) if (instanceIndex < 0) throw new ArgumentOutOfRangeException (nameof (instanceIndex)); HarfBuzzApi.hb_font_set_var_named_instance (Handle, (uint)instanceIndex); + GC.KeepAlive (this); } public void SetFunctionsOpenType () { HarfBuzzApi.hb_ot_font_set_funcs (Handle); + GC.KeepAlive (this); } public void Shape (Buffer buffer, params Feature[] features) => @@ -421,6 +457,7 @@ public void Shape (Buffer buffer, IReadOnlyList features, IReadOnlyList } GC.KeepAlive (buffer); + GC.KeepAlive (this); if (shapersPtrs != null) { for (var i = 0; i < shapersPtrs.Length; i++) { diff --git a/binding/HarfBuzzSharp/FontFunctions.cs b/binding/HarfBuzzSharp/FontFunctions.cs index 7f6e2353c39..001ff28086b 100644 --- a/binding/HarfBuzzSharp/FontFunctions.cs +++ b/binding/HarfBuzzSharp/FontFunctions.cs @@ -24,6 +24,7 @@ internal FontFunctions (IntPtr handle) public bool IsImmutable { get { var r = HarfBuzzApi.hb_font_funcs_is_immutable (Handle); + GC.KeepAlive (this); return r; } } @@ -31,6 +32,7 @@ public bool IsImmutable { public void MakeImmutable () { HarfBuzzApi.hb_font_funcs_make_immutable (Handle); + GC.KeepAlive (this); } public void SetHorizontalFontExtentsDelegate (FontExtentsDelegate del, ReleaseDelegate destroy = null) @@ -41,6 +43,7 @@ public void SetHorizontalFontExtentsDelegate (FontExtentsDelegate del, ReleaseDe HarfBuzzApi.hb_font_funcs_set_font_h_extents_func ( Handle, DelegateProxies.FontGetFontExtentsProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetVerticalFontExtentsDelegate (FontExtentsDelegate del, ReleaseDelegate destroy = null) @@ -51,6 +54,7 @@ public void SetVerticalFontExtentsDelegate (FontExtentsDelegate del, ReleaseDele HarfBuzzApi.hb_font_funcs_set_font_v_extents_func ( Handle, DelegateProxies.FontGetFontExtentsProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetNominalGlyphDelegate (NominalGlyphDelegate del, ReleaseDelegate destroy = null) @@ -61,6 +65,7 @@ public void SetNominalGlyphDelegate (NominalGlyphDelegate del, ReleaseDelegate d HarfBuzzApi.hb_font_funcs_set_nominal_glyph_func ( Handle, DelegateProxies.FontGetNominalGlyphProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetNominalGlyphsDelegate (NominalGlyphsDelegate del, ReleaseDelegate destroy = null) @@ -71,6 +76,7 @@ public void SetNominalGlyphsDelegate (NominalGlyphsDelegate del, ReleaseDelegate HarfBuzzApi.hb_font_funcs_set_nominal_glyphs_func ( Handle, DelegateProxies.FontGetNominalGlyphsProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetVariationGlyphDelegate (VariationGlyphDelegate del, ReleaseDelegate destroy = null) @@ -81,6 +87,7 @@ public void SetVariationGlyphDelegate (VariationGlyphDelegate del, ReleaseDelega HarfBuzzApi.hb_font_funcs_set_variation_glyph_func ( Handle, DelegateProxies.FontGetVariationGlyphProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetHorizontalGlyphAdvanceDelegate (GlyphAdvanceDelegate del, ReleaseDelegate destroy = null) @@ -91,6 +98,7 @@ public void SetHorizontalGlyphAdvanceDelegate (GlyphAdvanceDelegate del, Release HarfBuzzApi.hb_font_funcs_set_glyph_h_advance_func ( Handle, DelegateProxies.FontGetGlyphAdvanceProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetVerticalGlyphAdvanceDelegate (GlyphAdvanceDelegate del, ReleaseDelegate destroy = null) @@ -101,6 +109,7 @@ public void SetVerticalGlyphAdvanceDelegate (GlyphAdvanceDelegate del, ReleaseDe HarfBuzzApi.hb_font_funcs_set_glyph_v_advance_func ( Handle, DelegateProxies.FontGetGlyphAdvanceProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetHorizontalGlyphAdvancesDelegate (GlyphAdvancesDelegate del, ReleaseDelegate destroy = null) @@ -111,6 +120,7 @@ public void SetHorizontalGlyphAdvancesDelegate (GlyphAdvancesDelegate del, Relea HarfBuzzApi.hb_font_funcs_set_glyph_h_advances_func ( Handle, DelegateProxies.FontGetGlyphAdvancesProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetVerticalGlyphAdvancesDelegate (GlyphAdvancesDelegate del, ReleaseDelegate destroy = null) @@ -121,6 +131,7 @@ public void SetVerticalGlyphAdvancesDelegate (GlyphAdvancesDelegate del, Release HarfBuzzApi.hb_font_funcs_set_glyph_v_advances_func ( Handle, DelegateProxies.FontGetGlyphAdvancesProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetHorizontalGlyphOriginDelegate (GlyphOriginDelegate del, ReleaseDelegate destroy = null) @@ -131,6 +142,7 @@ public void SetHorizontalGlyphOriginDelegate (GlyphOriginDelegate del, ReleaseDe HarfBuzzApi.hb_font_funcs_set_glyph_h_origin_func ( Handle, DelegateProxies.FontGetGlyphOriginProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetVerticalGlyphOriginDelegate (GlyphOriginDelegate del, ReleaseDelegate destroy = null) @@ -141,6 +153,7 @@ public void SetVerticalGlyphOriginDelegate (GlyphOriginDelegate del, ReleaseDele HarfBuzzApi.hb_font_funcs_set_glyph_v_origin_func ( Handle, DelegateProxies.FontGetGlyphOriginProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetHorizontalGlyphKerningDelegate (GlyphKerningDelegate del, ReleaseDelegate destroy = null) @@ -151,6 +164,7 @@ public void SetHorizontalGlyphKerningDelegate (GlyphKerningDelegate del, Release HarfBuzzApi.hb_font_funcs_set_glyph_h_kerning_func ( Handle, DelegateProxies.FontGetGlyphKerningProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetGlyphExtentsDelegate (GlyphExtentsDelegate del, ReleaseDelegate destroy = null) @@ -161,6 +175,7 @@ public void SetGlyphExtentsDelegate (GlyphExtentsDelegate del, ReleaseDelegate d HarfBuzzApi.hb_font_funcs_set_glyph_extents_func ( Handle, DelegateProxies.FontGetGlyphExtentsProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetGlyphContourPointDelegate (GlyphContourPointDelegate del, ReleaseDelegate destroy = null) { @@ -170,6 +185,7 @@ public void SetGlyphContourPointDelegate (GlyphContourPointDelegate del, Release HarfBuzzApi.hb_font_funcs_set_glyph_contour_point_func ( Handle, DelegateProxies.FontGetGlyphContourPointProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetGlyphNameDelegate (GlyphNameDelegate del, ReleaseDelegate destroy = null) @@ -180,6 +196,7 @@ public void SetGlyphNameDelegate (GlyphNameDelegate del, ReleaseDelegate destroy HarfBuzzApi.hb_font_funcs_set_glyph_name_func ( Handle, DelegateProxies.FontGetGlyphNameProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetGlyphFromNameDelegate (GlyphFromNameDelegate del, ReleaseDelegate destroy = null) @@ -190,6 +207,7 @@ public void SetGlyphFromNameDelegate (GlyphFromNameDelegate del, ReleaseDelegate HarfBuzzApi.hb_font_funcs_set_glyph_from_name_func ( Handle, DelegateProxies.FontGetGlyphFromNameProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } protected override void Dispose (bool disposing) => diff --git a/binding/HarfBuzzSharp/UnicodeFunctions.cs b/binding/HarfBuzzSharp/UnicodeFunctions.cs index 7432259c19d..7228a5be4c3 100644 --- a/binding/HarfBuzzSharp/UnicodeFunctions.cs +++ b/binding/HarfBuzzSharp/UnicodeFunctions.cs @@ -37,6 +37,7 @@ public UnicodeFunctions (UnicodeFunctions parent) : base (IntPtr.Zero) public bool IsImmutable { get { var r = HarfBuzzApi.hb_unicode_funcs_is_immutable (Handle); + GC.KeepAlive (this); return r; } } @@ -44,6 +45,7 @@ public bool IsImmutable { public void MakeImmutable () { HarfBuzzApi.hb_unicode_funcs_make_immutable (Handle); + GC.KeepAlive (this); } public UnicodeCombiningClass GetCombiningClass (int unicode) => GetCombiningClass ((uint)unicode); @@ -51,6 +53,7 @@ public void MakeImmutable () public UnicodeCombiningClass GetCombiningClass (uint unicode) { var r = HarfBuzzApi.hb_unicode_combining_class (Handle, unicode); + GC.KeepAlive (this); return r; } @@ -59,6 +62,7 @@ public UnicodeCombiningClass GetCombiningClass (uint unicode) public UnicodeGeneralCategory GetGeneralCategory (uint unicode) { var r = HarfBuzzApi.hb_unicode_general_category (Handle, unicode); + GC.KeepAlive (this); return r; } @@ -67,6 +71,7 @@ public UnicodeGeneralCategory GetGeneralCategory (uint unicode) public uint GetMirroring (uint unicode) { var r = HarfBuzzApi.hb_unicode_mirroring (Handle, unicode); + GC.KeepAlive (this); return r; } @@ -75,6 +80,7 @@ public uint GetMirroring (uint unicode) public Script GetScript (uint unicode) { var r = HarfBuzzApi.hb_unicode_script (Handle, unicode); + GC.KeepAlive (this); return r; } @@ -91,6 +97,7 @@ public bool TryCompose (uint a, uint b, out uint ab) { fixed (uint* abPtr = &ab) { var r = HarfBuzzApi.hb_unicode_compose (Handle, a, b, abPtr); + GC.KeepAlive (this); return r; } } @@ -111,6 +118,7 @@ public bool TryDecompose (uint ab, out uint a, out uint b) fixed (uint* aPtr = &a) fixed (uint* bPtr = &b) { var r = HarfBuzzApi.hb_unicode_decompose (Handle, ab, aPtr, bPtr); + GC.KeepAlive (this); return r; } } @@ -122,6 +130,7 @@ public void SetCombiningClassDelegate (CombiningClassDelegate del, ReleaseDelega var ctx = DelegateProxies.CreateMultiUserData (del, destroy, this); HarfBuzzApi.hb_unicode_funcs_set_combining_class_func ( Handle, DelegateProxies.UnicodeCombiningClassProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetGeneralCategoryDelegate (GeneralCategoryDelegate del, ReleaseDelegate destroy = null) @@ -131,6 +140,7 @@ public void SetGeneralCategoryDelegate (GeneralCategoryDelegate del, ReleaseDele var ctx = DelegateProxies.CreateMultiUserData (del, destroy, this); HarfBuzzApi.hb_unicode_funcs_set_general_category_func ( Handle, DelegateProxies.UnicodeGeneralCategoryProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetMirroringDelegate (MirroringDelegate del, ReleaseDelegate destroy = null) @@ -140,6 +150,7 @@ public void SetMirroringDelegate (MirroringDelegate del, ReleaseDelegate destroy var ctx = DelegateProxies.CreateMultiUserData (del, destroy, this); HarfBuzzApi.hb_unicode_funcs_set_mirroring_func ( Handle, DelegateProxies.UnicodeMirroringProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetScriptDelegate (ScriptDelegate del, ReleaseDelegate destroy = null) @@ -149,6 +160,7 @@ public void SetScriptDelegate (ScriptDelegate del, ReleaseDelegate destroy = nul var ctx = DelegateProxies.CreateMultiUserData (del, destroy, this); HarfBuzzApi.hb_unicode_funcs_set_script_func ( Handle, DelegateProxies.UnicodeScriptProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetComposeDelegate (ComposeDelegate del, ReleaseDelegate destroy = null) @@ -158,6 +170,7 @@ public void SetComposeDelegate (ComposeDelegate del, ReleaseDelegate destroy = n var ctx = DelegateProxies.CreateMultiUserData (del, destroy, this); HarfBuzzApi.hb_unicode_funcs_set_compose_func ( Handle, DelegateProxies.UnicodeComposeProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } public void SetDecomposeDelegate (DecomposeDelegate del, ReleaseDelegate destroy = null) @@ -167,6 +180,7 @@ public void SetDecomposeDelegate (DecomposeDelegate del, ReleaseDelegate destroy var ctx = DelegateProxies.CreateMultiUserData (del, destroy, this); HarfBuzzApi.hb_unicode_funcs_set_decompose_func ( Handle, DelegateProxies.UnicodeDecomposeProxy, (void*)ctx, DelegateProxies.DestroyProxyForMulti); + GC.KeepAlive (this); } private void VerifyParameters (Delegate del) diff --git a/binding/SkiaSharp/GRBackendRenderTarget.cs b/binding/SkiaSharp/GRBackendRenderTarget.cs index c345f35288a..7faee0e8164 100644 --- a/binding/SkiaSharp/GRBackendRenderTarget.cs +++ b/binding/SkiaSharp/GRBackendRenderTarget.cs @@ -94,36 +94,42 @@ protected override void DisposeNative () => public bool IsValid { get { var result = SkiaApi.gr_backendrendertarget_is_valid (Handle); + GC.KeepAlive (this); return result; } } public int Width { get { var result = SkiaApi.gr_backendrendertarget_get_width (Handle); + GC.KeepAlive (this); return result; } } public int Height { get { var result = SkiaApi.gr_backendrendertarget_get_height (Handle); + GC.KeepAlive (this); return result; } } public int SampleCount { get { var result = SkiaApi.gr_backendrendertarget_get_samples (Handle); + GC.KeepAlive (this); return result; } } public int StencilBits { get { var result = SkiaApi.gr_backendrendertarget_get_stencils (Handle); + GC.KeepAlive (this); return result; } } public GRBackend Backend { get { var result = SkiaApi.gr_backendrendertarget_get_backend (Handle).FromNative (); + GC.KeepAlive (this); return result; } } @@ -137,6 +143,7 @@ public bool GetGlFramebufferInfo (out GRGlFramebufferInfo glInfo) { fixed (GRGlFramebufferInfo* g = &glInfo) { var result = SkiaApi.gr_backendrendertarget_get_gl_framebufferinfo (Handle, g); + GC.KeepAlive (this); return result; } } diff --git a/binding/SkiaSharp/GRBackendTexture.cs b/binding/SkiaSharp/GRBackendTexture.cs index 4f920bbbd30..c58ca33e312 100644 --- a/binding/SkiaSharp/GRBackendTexture.cs +++ b/binding/SkiaSharp/GRBackendTexture.cs @@ -79,30 +79,35 @@ protected override void DisposeNative () => public bool IsValid { get { var result = SkiaApi.gr_backendtexture_is_valid (Handle); + GC.KeepAlive (this); return result; } } public int Width { get { var result = SkiaApi.gr_backendtexture_get_width (Handle); + GC.KeepAlive (this); return result; } } public int Height { get { var result = SkiaApi.gr_backendtexture_get_height (Handle); + GC.KeepAlive (this); return result; } } public bool HasMipMaps { get { var result = SkiaApi.gr_backendtexture_has_mipmaps (Handle); + GC.KeepAlive (this); return result; } } public GRBackend Backend { get { var result = SkiaApi.gr_backendtexture_get_backend (Handle).FromNative (); + GC.KeepAlive (this); return result; } } @@ -116,6 +121,7 @@ public bool GetGlTextureInfo (out GRGlTextureInfo glInfo) { fixed (GRGlTextureInfo* g = &glInfo) { var result = SkiaApi.gr_backendtexture_get_gl_textureinfo (Handle, g); + GC.KeepAlive (this); return result; } } diff --git a/binding/SkiaSharp/GRContext.cs b/binding/SkiaSharp/GRContext.cs index 326c3553079..1845a7b9264 100644 --- a/binding/SkiaSharp/GRContext.cs +++ b/binding/SkiaSharp/GRContext.cs @@ -110,6 +110,7 @@ public static GRContext CreateMetal (GRMtlBackendContext backendContext, GRConte public override bool IsAbandoned { get { var result = SkiaApi.gr_direct_context_is_abandoned (Handle); + GC.KeepAlive (this); return result; } } @@ -120,17 +121,20 @@ public void AbandonContext (bool releaseResources = false) SkiaApi.gr_direct_context_release_resources_and_abandon_context (Handle); else SkiaApi.gr_direct_context_abandon_context (Handle); + GC.KeepAlive (this); } public long GetResourceCacheLimit () { var result = (long)SkiaApi.gr_direct_context_get_resource_cache_limit (Handle); + GC.KeepAlive (this); return result; } public void SetResourceCacheLimit (long maxResourceBytes) { SkiaApi.gr_direct_context_set_resource_cache_limit (Handle, (IntPtr)maxResourceBytes); + GC.KeepAlive (this); } public void GetResourceCacheUsage (out int maxResources, out long maxResourceBytes) @@ -138,6 +142,7 @@ public void GetResourceCacheUsage (out int maxResources, out long maxResourceByt IntPtr maxResBytes; fixed (int* maxRes = &maxResources) { SkiaApi.gr_direct_context_get_resource_cache_usage (Handle, maxRes, &maxResBytes); + GC.KeepAlive (this); } maxResourceBytes = (long)maxResBytes; } @@ -151,6 +156,7 @@ public void ResetContext (GRBackendState state = GRBackendState.All) => public void ResetContext (uint state) { SkiaApi.gr_direct_context_reset_context (Handle, state); + GC.KeepAlive (this); } public void Flush () => Flush (true); @@ -161,11 +167,13 @@ public void Flush (bool submit, bool synchronous = false) SkiaApi.gr_direct_context_flush_and_submit (Handle, synchronous); else SkiaApi.gr_direct_context_flush (Handle); + GC.KeepAlive (this); } public void Submit (bool synchronous = false) { SkiaApi.gr_direct_context_submit (Handle, synchronous); + GC.KeepAlive (this); } public void Flush (SKImage image) @@ -176,6 +184,7 @@ public void Flush (SKImage image) SkiaApi.gr_direct_context_flush_image (Handle, image.Handle); GC.KeepAlive (image); + GC.KeepAlive (this); } public void Flush (SKSurface surface) @@ -186,6 +195,7 @@ public void Flush (SKSurface surface) SkiaApi.gr_direct_context_flush_surface (Handle, surface.Handle); GC.KeepAlive (surface); + GC.KeepAlive (this); } public new int GetMaxSurfaceSampleCount (SKColorType colorType) => @@ -195,26 +205,31 @@ public void DumpMemoryStatistics (SKTraceMemoryDump dump) { SkiaApi.gr_direct_context_dump_memory_statistics (Handle, dump?.Handle ?? throw new ArgumentNullException (nameof (dump))); GC.KeepAlive (dump); + GC.KeepAlive (this); } public void PurgeResources () { SkiaApi.gr_direct_context_free_gpu_resources (Handle); + GC.KeepAlive (this); } public void PurgeUnusedResources (long milliseconds) { SkiaApi.gr_direct_context_perform_deferred_cleanup (Handle, milliseconds); + GC.KeepAlive (this); } public void PurgeUnlockedResources (bool scratchResourcesOnly) { SkiaApi.gr_direct_context_purge_unlocked_resources (Handle, scratchResourcesOnly); + GC.KeepAlive (this); } public void PurgeUnlockedResources (long bytesToPurge, bool preferScratchResources) { SkiaApi.gr_direct_context_purge_unlocked_resources_bytes (Handle, (IntPtr)bytesToPurge, preferScratchResources); + GC.KeepAlive (this); } internal new static GRContext GetObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => diff --git a/binding/SkiaSharp/GRGlInterface.cs b/binding/SkiaSharp/GRGlInterface.cs index 4fb6d00bd1b..f7fbb08cc5b 100644 --- a/binding/SkiaSharp/GRGlInterface.cs +++ b/binding/SkiaSharp/GRGlInterface.cs @@ -111,12 +111,14 @@ public static GRGlInterface CreateEvas (IntPtr evas) public bool Validate () { var result = SkiaApi.gr_glinterface_validate (Handle); + GC.KeepAlive (this); return result; } public bool HasExtension (string extension) { var result = SkiaApi.gr_glinterface_has_extension (Handle, extension); + GC.KeepAlive (this); return result; } diff --git a/binding/SkiaSharp/GRRecordingContext.cs b/binding/SkiaSharp/GRRecordingContext.cs index 3c32a47e81d..14af9eed62e 100644 --- a/binding/SkiaSharp/GRRecordingContext.cs +++ b/binding/SkiaSharp/GRRecordingContext.cs @@ -14,6 +14,7 @@ internal GRRecordingContext (IntPtr h, bool owns) public virtual GRBackend Backend { get { var result = SkiaApi.gr_recording_context_get_backend (Handle).FromNative (); + GC.KeepAlive (this); return result; } } @@ -21,6 +22,7 @@ public virtual GRBackend Backend { public virtual bool IsAbandoned { get { var result = SkiaApi.gr_recording_context_is_abandoned (Handle); + GC.KeepAlive (this); return result; } } @@ -28,6 +30,7 @@ public virtual bool IsAbandoned { public int MaxTextureSize { get { var result = SkiaApi.gr_recording_context_max_texture_size (Handle); + GC.KeepAlive (this); return result; } } @@ -35,6 +38,7 @@ public int MaxTextureSize { public int MaxRenderTargetSize { get { var result = SkiaApi.gr_recording_context_max_render_target_size (Handle); + GC.KeepAlive (this); return result; } } @@ -42,6 +46,7 @@ public int MaxRenderTargetSize { public int GetMaxSurfaceSampleCount (SKColorType colorType) { var result = SkiaApi.gr_recording_context_get_max_surface_sample_count_for_color_type (Handle, colorType.ToNative ()); + GC.KeepAlive (this); return result; } diff --git a/binding/SkiaSharp/SKBitmap.cs b/binding/SkiaSharp/SKBitmap.cs index 897fbeeb388..1a18d1216c8 100644 --- a/binding/SkiaSharp/SKBitmap.cs +++ b/binding/SkiaSharp/SKBitmap.cs @@ -82,6 +82,7 @@ public bool TryAllocPixels (SKImageInfo info, int rowBytes) { var cinfo = SKImageInfoNative.FromManaged (ref info); var result = SkiaApi.sk_bitmap_try_alloc_pixels (Handle, &cinfo, (IntPtr)rowBytes); + GC.KeepAlive (this); return result; } @@ -89,6 +90,7 @@ public bool TryAllocPixels (SKImageInfo info, SKBitmapAllocFlags flags) { var cinfo = SKImageInfoNative.FromManaged (ref info); var result = SkiaApi.sk_bitmap_try_alloc_pixels_with_flags (Handle, &cinfo, (uint)flags); + GC.KeepAlive (this); return result; } @@ -97,6 +99,7 @@ public bool TryAllocPixels (SKImageInfo info, SKBitmapAllocFlags flags) public void Reset () { SkiaApi.sk_bitmap_reset (Handle); + GC.KeepAlive (this); } // SetImmutable @@ -104,6 +107,7 @@ public void Reset () public void SetImmutable () { SkiaApi.sk_bitmap_set_immutable (Handle); + GC.KeepAlive (this); } // Erase @@ -111,11 +115,13 @@ public void SetImmutable () public void Erase (SKColor color) { SkiaApi.sk_bitmap_erase (Handle, (uint)color); + GC.KeepAlive (this); } public void Erase (SKColor color, SKRectI rect) { SkiaApi.sk_bitmap_erase_rect (Handle, (uint)color, &rect); + GC.KeepAlive (this); } // GetAddress @@ -123,6 +129,7 @@ public void Erase (SKColor color, SKRectI rect) public IntPtr GetAddress (int x, int y) { var result = (IntPtr)SkiaApi.sk_bitmap_get_addr (Handle, x, y); + GC.KeepAlive (this); return result; } @@ -131,6 +138,7 @@ public IntPtr GetAddress (int x, int y) public SKColor GetPixel (int x, int y) { var result = SkiaApi.sk_bitmap_get_pixel_color (Handle, x, y); + GC.KeepAlive (this); return result; } @@ -225,6 +233,7 @@ public bool ExtractSubset (SKBitmap destination, SKRectI subset) throw new ArgumentNullException (nameof (destination)); } var result = SkiaApi.sk_bitmap_extract_subset (Handle, destination.Handle, &subset); + GC.KeepAlive (this); GC.KeepAlive (destination); return result; } @@ -253,6 +262,7 @@ public bool ExtractAlpha (SKBitmap destination, SKPaint paint, out SKPointI offs } fixed (SKPointI* o = &offset) { var result = SkiaApi.sk_bitmap_extract_alpha (Handle, destination.Handle, paint == null ? IntPtr.Zero : paint.Handle, o); + GC.KeepAlive (this); GC.KeepAlive (destination); GC.KeepAlive (paint); return result; @@ -264,6 +274,7 @@ public bool ExtractAlpha (SKBitmap destination, SKPaint paint, out SKPointI offs public bool ReadyToDraw { get { var result = SkiaApi.sk_bitmap_ready_to_draw (Handle); + GC.KeepAlive (this); return result; } } @@ -272,6 +283,7 @@ public SKImageInfo Info { get { SKImageInfoNative cinfo; SkiaApi.sk_bitmap_get_info (Handle, &cinfo); + GC.KeepAlive (this); return SKImageInfoNative.ToManaged (ref cinfo); } } @@ -303,6 +315,7 @@ public int BytesPerPixel { public int RowBytes { get { var result = (int)SkiaApi.sk_bitmap_get_row_bytes (Handle); + GC.KeepAlive (this); return result; } } @@ -310,6 +323,7 @@ public int RowBytes { public int ByteCount { get { var result = (int)SkiaApi.sk_bitmap_get_byte_count (Handle); + GC.KeepAlive (this); return result; } } @@ -329,6 +343,7 @@ public IntPtr GetPixels (out IntPtr length) { fixed (IntPtr* l = &length) { var result = (IntPtr)SkiaApi.sk_bitmap_get_pixels (Handle, l); + GC.KeepAlive (this); return result; } } @@ -336,6 +351,7 @@ public IntPtr GetPixels (out IntPtr length) public void SetPixels (IntPtr pixels) { SkiaApi.sk_bitmap_set_pixels (Handle, (void*)pixels); + GC.KeepAlive (this); } // more properties @@ -343,6 +359,7 @@ public void SetPixels (IntPtr pixels) public byte[] Bytes { get { var array = GetPixelSpan ().ToArray (); + GC.KeepAlive (this); return array; } } @@ -353,6 +370,7 @@ public SKColor[] Pixels { var pixels = new SKColor[checked(info.Width * info.Height)]; fixed (SKColor* p = pixels) { SkiaApi.sk_bitmap_get_pixel_colors (Handle, (uint*)p); + GC.KeepAlive (this); } return pixels; } @@ -389,6 +407,7 @@ public bool IsEmpty { public bool IsNull { get { var result = SkiaApi.sk_bitmap_is_null (Handle); + GC.KeepAlive (this); return result; } } @@ -400,6 +419,7 @@ public bool DrawsNothing { public bool IsImmutable { get { var result = SkiaApi.sk_bitmap_is_immutable (Handle); + GC.KeepAlive (this); return result; } } @@ -641,12 +661,14 @@ public bool InstallPixels (SKImageInfo info, IntPtr pixels, int rowBytes, SKBitm DelegateProxies.Create (del, out _, out var ctx); var proxy = del is not null ? DelegateProxies.SKBitmapReleaseProxy : null; var result = SkiaApi.sk_bitmap_install_pixels (Handle, &cinfo, (void*)pixels, (IntPtr)rowBytes, proxy, (void*)ctx); + GC.KeepAlive (this); return result; } public bool InstallPixels (SKPixmap pixmap) { var result = SkiaApi.sk_bitmap_install_pixels_with_pixmap (Handle, pixmap.Handle); + GC.KeepAlive (this); GC.KeepAlive (pixmap); return result; } @@ -656,6 +678,7 @@ public bool InstallPixels (SKPixmap pixmap) public void NotifyPixelsChanged () { SkiaApi.sk_bitmap_notify_pixels_changed (Handle); + GC.KeepAlive (this); } // PeekPixels @@ -678,6 +701,7 @@ public bool PeekPixels (SKPixmap pixmap) throw new ArgumentNullException (nameof (pixmap)); } var result = SkiaApi.sk_bitmap_peek_pixels (Handle, pixmap.Handle); + GC.KeepAlive (this); GC.KeepAlive (pixmap); if (result) pixmap.pixelSource = this; @@ -788,6 +812,7 @@ public bool Encode (SKWStream dst, SKEncodedImageFormat format, int quality) private void Swap (SKBitmap other) { SkiaApi.sk_bitmap_swap (Handle, other.Handle); + GC.KeepAlive (this); GC.KeepAlive (other); } @@ -818,6 +843,7 @@ public SKShader ToShader (SKShaderTileMode tmx, SKShaderTileMode tmy, SKFilterQu private SKShader ToShader (SKShaderTileMode tmx, SKShaderTileMode tmy, SKSamplingOptions sampling, SKMatrix* localMatrix) { var result = SKShader.GetObject (SkiaApi.sk_bitmap_make_shader (Handle, tmx, tmy, &sampling, localMatrix)); + GC.KeepAlive (this); return result; } } diff --git a/binding/SkiaSharp/SKCanvas.cs b/binding/SkiaSharp/SKCanvas.cs index 02c4971ea61..c20c54bd71f 100644 --- a/binding/SkiaSharp/SKCanvas.cs +++ b/binding/SkiaSharp/SKCanvas.cs @@ -36,6 +36,7 @@ protected override void DisposeNative () => public void Discard () { SkiaApi.sk_canvas_discard (Handle); + GC.KeepAlive (this); } // QuickReject @@ -43,6 +44,7 @@ public void Discard () public bool QuickReject (SKRect rect) { var result = SkiaApi.sk_canvas_quick_reject (Handle, &rect); + GC.KeepAlive (this); return result; } @@ -61,6 +63,7 @@ public int Save () if (Handle == IntPtr.Zero) throw new ObjectDisposedException ("SKCanvas"); var result = SkiaApi.sk_canvas_save (Handle); + GC.KeepAlive (this); return result; } @@ -68,6 +71,7 @@ public int SaveLayer (SKRect limit, SKPaint? paint) { var result = SkiaApi.sk_canvas_save_layer (Handle, &limit, paint?.Handle ?? IntPtr.Zero); GC.KeepAlive (paint); + GC.KeepAlive (this); return result; } @@ -75,6 +79,7 @@ public int SaveLayer (SKPaint? paint) { var result = SkiaApi.sk_canvas_save_layer (Handle, null, paint?.Handle ?? IntPtr.Zero); GC.KeepAlive (paint); + GC.KeepAlive (this); return result; } @@ -82,12 +87,14 @@ public int SaveLayer (in SKCanvasSaveLayerRec rec) { var native = rec.ToNative (); var result = SkiaApi.sk_canvas_save_layer_rec (Handle, &native); + GC.KeepAlive (this); return result; } public int SaveLayer () { var result = SkiaApi.sk_canvas_save_layer (Handle, null, IntPtr.Zero); + GC.KeepAlive (this); return result; } #nullable disable @@ -97,11 +104,13 @@ public int SaveLayer () public void DrawColor (SKColor color, SKBlendMode mode = SKBlendMode.Src) { SkiaApi.sk_canvas_draw_color (Handle, (uint)color, mode); + GC.KeepAlive (this); } public void DrawColor (SKColorF color, SKBlendMode mode = SKBlendMode.Src) { SkiaApi.sk_canvas_draw_color4f (Handle, color, mode); + GC.KeepAlive (this); } // DrawLine @@ -117,6 +126,7 @@ public void DrawLine (float x0, float y0, float x1, float y1, SKPaint paint) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_line (Handle, x0, y0, x1, y1, paint.Handle); GC.KeepAlive (paint); + GC.KeepAlive (this); } // Clear @@ -127,11 +137,13 @@ public void Clear () => public void Clear (SKColor color) { SkiaApi.sk_canvas_clear (Handle, (uint)color); + GC.KeepAlive (this); } public void Clear (SKColorF color) { SkiaApi.sk_canvas_clear_color4f (Handle, color); + GC.KeepAlive (this); } // Restore* @@ -139,11 +151,13 @@ public void Clear (SKColorF color) public void Restore () { SkiaApi.sk_canvas_restore (Handle); + GC.KeepAlive (this); } public void RestoreToCount (int count) { SkiaApi.sk_canvas_restore_to_count (Handle, count); + GC.KeepAlive (this); } // Translate @@ -154,6 +168,7 @@ public void Translate (float dx, float dy) return; SkiaApi.sk_canvas_translate (Handle, dx, dy); + GC.KeepAlive (this); } public void Translate (SKPoint point) @@ -162,6 +177,7 @@ public void Translate (SKPoint point) return; SkiaApi.sk_canvas_translate (Handle, point.X, point.Y); + GC.KeepAlive (this); } // Scale @@ -172,6 +188,7 @@ public void Scale (float s) return; SkiaApi.sk_canvas_scale (Handle, s, s); + GC.KeepAlive (this); } public void Scale (float sx, float sy) @@ -180,6 +197,7 @@ public void Scale (float sx, float sy) return; SkiaApi.sk_canvas_scale (Handle, sx, sy); + GC.KeepAlive (this); } public void Scale (SKPoint size) @@ -188,6 +206,7 @@ public void Scale (SKPoint size) return; SkiaApi.sk_canvas_scale (Handle, size.X, size.Y); + GC.KeepAlive (this); } public void Scale (float sx, float sy, float px, float py) @@ -208,6 +227,7 @@ public void RotateDegrees (float degrees) return; SkiaApi.sk_canvas_rotate_degrees (Handle, degrees); + GC.KeepAlive (this); } public void RotateRadians (float radians) @@ -216,6 +236,7 @@ public void RotateRadians (float radians) return; SkiaApi.sk_canvas_rotate_radians (Handle, radians); + GC.KeepAlive (this); } public void RotateDegrees (float degrees, float px, float py) @@ -246,6 +267,7 @@ public void Skew (float sx, float sy) return; SkiaApi.sk_canvas_skew (Handle, sx, sy); + GC.KeepAlive (this); } public void Skew (SKPoint skew) @@ -254,6 +276,7 @@ public void Skew (SKPoint skew) return; SkiaApi.sk_canvas_skew (Handle, skew.X, skew.Y); + GC.KeepAlive (this); } // Concat @@ -266,6 +289,7 @@ public void Concat (in SKMatrix44 m) fixed (SKMatrix44* ptr = &m) { SkiaApi.sk_canvas_concat (Handle, ptr); } + GC.KeepAlive (this); } // Clip* @@ -273,6 +297,7 @@ public void Concat (in SKMatrix44 m) public void ClipRect (SKRect rect, SKClipOperation operation = SKClipOperation.Intersect, bool antialias = false) { SkiaApi.sk_canvas_clip_rect_with_operation (Handle, &rect, operation, antialias); + GC.KeepAlive (this); } public void ClipRoundRect (SKRoundRect rect, SKClipOperation operation = SKClipOperation.Intersect, bool antialias = false) @@ -282,6 +307,7 @@ public void ClipRoundRect (SKRoundRect rect, SKClipOperation operation = SKClipO SkiaApi.sk_canvas_clip_rrect_with_operation (Handle, rect.Handle, operation, antialias); GC.KeepAlive (rect); + GC.KeepAlive (this); } public void ClipPath (SKPath path, SKClipOperation operation = SKClipOperation.Intersect, bool antialias = false) @@ -291,6 +317,7 @@ public void ClipPath (SKPath path, SKClipOperation operation = SKClipOperation.I SkiaApi.sk_canvas_clip_path_with_operation (Handle, path.Handle, operation, antialias); GC.KeepAlive (path); + GC.KeepAlive (this); } public void ClipRegion (SKRegion region, SKClipOperation operation = SKClipOperation.Intersect) @@ -300,6 +327,7 @@ public void ClipRegion (SKRegion region, SKClipOperation operation = SKClipOpera SkiaApi.sk_canvas_clip_region (Handle, region.Handle, operation); GC.KeepAlive (region); + GC.KeepAlive (this); } public SKRect LocalClipBounds { @@ -319,6 +347,7 @@ public SKRectI DeviceClipBounds { public bool IsClipEmpty { get { var result = SkiaApi.sk_canvas_is_clip_empty (Handle); + GC.KeepAlive (this); return result; } } @@ -326,6 +355,7 @@ public bool IsClipEmpty { public bool IsClipRect { get { var result = SkiaApi.sk_canvas_is_clip_rect (Handle); + GC.KeepAlive (this); return result; } } @@ -334,6 +364,7 @@ public bool GetLocalClipBounds (out SKRect bounds) { fixed (SKRect* b = &bounds) { var result = SkiaApi.sk_canvas_get_local_clip_bounds (Handle, b); + GC.KeepAlive (this); return result; } } @@ -342,6 +373,7 @@ public bool GetDeviceClipBounds (out SKRectI bounds) { fixed (SKRectI* b = &bounds) { var result = SkiaApi.sk_canvas_get_device_clip_bounds (Handle, b); + GC.KeepAlive (this); return result; } } @@ -354,6 +386,7 @@ public void DrawPaint (SKPaint paint) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_paint (Handle, paint.Handle); GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawRegion @@ -367,6 +400,7 @@ public void DrawRegion (SKRegion region, SKPaint paint) SkiaApi.sk_canvas_draw_region (Handle, region.Handle, paint.Handle); GC.KeepAlive (region); GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawRect @@ -382,6 +416,7 @@ public void DrawRect (SKRect rect, SKPaint paint) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_rect (Handle, &rect, paint.Handle); GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawRoundRect @@ -395,6 +430,7 @@ public void DrawRoundRect (SKRoundRect rect, SKPaint paint) SkiaApi.sk_canvas_draw_rrect (Handle, rect.Handle, paint.Handle); GC.KeepAlive (rect); GC.KeepAlive (paint); + GC.KeepAlive (this); } public void DrawRoundRect (float x, float y, float w, float h, float rx, float ry, SKPaint paint) @@ -408,6 +444,7 @@ public void DrawRoundRect (SKRect rect, float rx, float ry, SKPaint paint) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_round_rect (Handle, &rect, rx, ry, paint.Handle); GC.KeepAlive (paint); + GC.KeepAlive (this); } public void DrawRoundRect (SKRect rect, SKSize r, SKPaint paint) @@ -433,6 +470,7 @@ public void DrawOval (SKRect rect, SKPaint paint) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_oval (Handle, &rect, paint.Handle); GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawCircle @@ -443,6 +481,7 @@ public void DrawCircle (float cx, float cy, float radius, SKPaint paint) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_circle (Handle, cx, cy, radius, paint.Handle); GC.KeepAlive (paint); + GC.KeepAlive (this); } public void DrawCircle (SKPoint c, float radius, SKPaint paint) @@ -461,6 +500,7 @@ public void DrawPath (SKPath path, SKPaint paint) SkiaApi.sk_canvas_draw_path (Handle, path.Handle, paint.Handle); GC.KeepAlive (path); GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawPoints @@ -475,6 +515,7 @@ public void DrawPoints (SKPointMode mode, SKPoint[] points, SKPaint paint) SkiaApi.sk_canvas_draw_points (Handle, mode, (IntPtr)points.Length, p, paint.Handle); } GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawPoint @@ -490,6 +531,7 @@ public void DrawPoint (float x, float y, SKPaint paint) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_point (Handle, x, y, paint.Handle); GC.KeepAlive (paint); + GC.KeepAlive (this); } public void DrawPoint (SKPoint p, SKColor color) @@ -528,6 +570,7 @@ public void DrawImage (SKImage image, float x, float y, SKSamplingOptions sampli SkiaApi.sk_canvas_draw_image (Handle, image.Handle, x, y, &sampling, paint?.Handle ?? IntPtr.Zero); GC.KeepAlive (image); GC.KeepAlive (paint); + GC.KeepAlive (this); } public void DrawImage (SKImage image, SKRect dest, SKPaint paint = null) @@ -557,6 +600,7 @@ private void DrawImage (SKImage image, SKRect* source, SKRect* dest, SKSamplingO SkiaApi.sk_canvas_draw_image_rect (Handle, image.Handle, source, dest, &sampling, paint?.Handle ?? IntPtr.Zero); GC.KeepAlive (image); GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawPicture @@ -580,6 +624,7 @@ public void DrawPicture (SKPicture picture, in SKMatrix matrix, SKPaint paint = SkiaApi.sk_canvas_draw_picture (Handle, picture.Handle, m, paint == null ? IntPtr.Zero : paint.Handle); GC.KeepAlive (picture); GC.KeepAlive (paint); + GC.KeepAlive (this); } public void DrawPicture (SKPicture picture, SKPaint paint = null) @@ -589,6 +634,7 @@ public void DrawPicture (SKPicture picture, SKPaint paint = null) SkiaApi.sk_canvas_draw_picture (Handle, picture.Handle, null, paint == null ? IntPtr.Zero : paint.Handle); GC.KeepAlive (picture); GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawDrawable @@ -600,6 +646,7 @@ public void DrawDrawable (SKDrawable drawable, in SKMatrix matrix) fixed (SKMatrix* m = &matrix) SkiaApi.sk_canvas_draw_drawable (Handle, drawable.Handle, m); GC.KeepAlive (drawable); + GC.KeepAlive (this); } public void DrawDrawable (SKDrawable drawable, float x, float y) @@ -681,6 +728,7 @@ public void DrawText (SKTextBlob text, float x, float y, SKPaint paint) SkiaApi.sk_canvas_draw_text_blob (Handle, text.Handle, x, y, paint.Handle); GC.KeepAlive (text); GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawText @@ -783,6 +831,7 @@ public void DrawTextOnPath (string text, SKPath path, SKPoint offset, bool warpG public SKSurface? Surface { get { var result = SKSurface.GetObject (SkiaApi.sk_get_surface (Handle), owns: false, unrefExisting: false); + GC.KeepAlive (this); return result; } } @@ -794,6 +843,7 @@ public SKSurface? Surface { public GRRecordingContext? Context { get { var result = GRRecordingContext.GetObject (SkiaApi.sk_get_recording_context (Handle), owns: false, unrefExisting: false); + GC.KeepAlive (this); return result; } } @@ -815,12 +865,14 @@ public void DrawAnnotation (SKRect rect, string key, SKData value) SkiaApi.sk_canvas_draw_annotation (base.Handle, &rect, b, value == null ? IntPtr.Zero : value.Handle); } GC.KeepAlive (value); + GC.KeepAlive (this); } public void DrawUrlAnnotation (SKRect rect, SKData value) { SkiaApi.sk_canvas_draw_url_annotation (Handle, &rect, value == null ? IntPtr.Zero : value.Handle); GC.KeepAlive (value); + GC.KeepAlive (this); } public SKData DrawUrlAnnotation (SKRect rect, string value) @@ -834,6 +886,7 @@ public void DrawNamedDestinationAnnotation (SKPoint point, SKData value) { SkiaApi.sk_canvas_draw_named_destination_annotation (Handle, &point, value == null ? IntPtr.Zero : value.Handle); GC.KeepAlive (value); + GC.KeepAlive (this); } public SKData DrawNamedDestinationAnnotation (SKPoint point, string value) @@ -847,6 +900,7 @@ public void DrawLinkDestinationAnnotation (SKRect rect, SKData value) { SkiaApi.sk_canvas_draw_link_destination_annotation (Handle, &rect, value == null ? IntPtr.Zero : value.Handle); GC.KeepAlive (value); + GC.KeepAlive (this); } public SKData DrawLinkDestinationAnnotation (SKRect rect, string value) @@ -881,6 +935,7 @@ public void DrawImageNinePatch (SKImage image, SKRectI center, SKRect dst, SKFil SkiaApi.sk_canvas_draw_image_nine (Handle, image.Handle, ¢er, &dst, filterMode, paint == null ? IntPtr.Zero : paint.Handle); GC.KeepAlive (image); GC.KeepAlive (paint); + GC.KeepAlive (this); } // Draw*Lattice @@ -948,6 +1003,7 @@ public void DrawImageLattice (SKImage image, SKLattice lattice, SKRect dst, SKFi } GC.KeepAlive (image); GC.KeepAlive (paint); + GC.KeepAlive (this); } // *Matrix @@ -955,6 +1011,7 @@ public void DrawImageLattice (SKImage image, SKLattice lattice, SKRect dst, SKFi public void ResetMatrix () { SkiaApi.sk_canvas_reset_matrix (Handle); + GC.KeepAlive (this); } public void SetMatrix (in SKMatrix matrix) => @@ -969,6 +1026,7 @@ public void SetMatrix (in SKMatrix44 matrix) fixed (SKMatrix44* ptr = &matrix) { SkiaApi.sk_canvas_set_matrix (Handle, ptr); } + GC.KeepAlive (this); } public SKMatrix TotalMatrix => TotalMatrix44.Matrix; @@ -977,6 +1035,7 @@ public SKMatrix44 TotalMatrix44 { get { SKMatrix44 matrix; SkiaApi.sk_canvas_get_matrix (Handle, &matrix); + GC.KeepAlive (this); return matrix; } } @@ -986,6 +1045,7 @@ public SKMatrix44 TotalMatrix44 { public int SaveCount { get { var result = SkiaApi.sk_canvas_get_save_count (Handle); + GC.KeepAlive (this); return result; } } @@ -1025,6 +1085,7 @@ public void DrawVertices (SKVertices vertices, SKBlendMode mode, SKPaint paint) SkiaApi.sk_canvas_draw_vertices (Handle, vertices.Handle, mode, paint.Handle); GC.KeepAlive (vertices); GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawArc @@ -1035,6 +1096,7 @@ public void DrawArc (SKRect oval, float startAngle, float sweepAngle, bool useCe throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_arc (Handle, &oval, startAngle, sweepAngle, useCenter, paint.Handle); GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawRoundRectDifference @@ -1052,6 +1114,7 @@ public void DrawRoundRectDifference (SKRoundRect outer, SKRoundRect inner, SKPai GC.KeepAlive (outer); GC.KeepAlive (inner); GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawAtlas @@ -1095,6 +1158,7 @@ private void DrawAtlas (SKImage atlas, SKRect[] sprites, SKRotationScaleMatrix[] } GC.KeepAlive (atlas); GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawPatch @@ -1124,6 +1188,7 @@ public void DrawPatch (SKPoint[] cubics, SKColor[] colors, SKPoint[] texCoords, SkiaApi.sk_canvas_draw_patch (Handle, cubes, (uint*)cols, coords, mode, paint.Handle); } GC.KeepAlive (paint); + GC.KeepAlive (this); } internal static SKCanvas GetObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => diff --git a/binding/SkiaSharp/SKCodec.cs b/binding/SkiaSharp/SKCodec.cs index ec8002bf740..c6b9db53376 100644 --- a/binding/SkiaSharp/SKCodec.cs +++ b/binding/SkiaSharp/SKCodec.cs @@ -29,6 +29,7 @@ public SKImageInfo Info { get { SKImageInfoNative cinfo; SkiaApi.sk_codec_get_info (Handle, &cinfo); + GC.KeepAlive (this); return SKImageInfoNative.ToManaged (ref cinfo); } } @@ -36,6 +37,7 @@ public SKImageInfo Info { public SKEncodedOrigin EncodedOrigin { get { var result = SkiaApi.sk_codec_get_origin (Handle); + GC.KeepAlive (this); return result; } } @@ -43,6 +45,7 @@ public SKEncodedOrigin EncodedOrigin { public SKEncodedImageFormat EncodedFormat { get { var result = SkiaApi.sk_codec_get_encoded_format (Handle); + GC.KeepAlive (this); return result; } } @@ -51,6 +54,7 @@ public SKSizeI GetScaledDimensions (float desiredScale) { SKSizeI dimensions; SkiaApi.sk_codec_get_scaled_dimensions (Handle, desiredScale, &dimensions); + GC.KeepAlive (this); return dimensions; } @@ -58,6 +62,7 @@ public bool GetValidSubset (ref SKRectI desiredSubset) { fixed (SKRectI* ds = &desiredSubset) { var result = SkiaApi.sk_codec_get_valid_subset (Handle, ds); + GC.KeepAlive (this); return result; } } @@ -77,6 +82,7 @@ public byte[] Pixels { public int RepetitionCount { get { var result = SkiaApi.sk_codec_get_repetition_count (Handle); + GC.KeepAlive (this); return result; } } @@ -84,6 +90,7 @@ public int RepetitionCount { public int FrameCount { get { var result = SkiaApi.sk_codec_get_frame_count (Handle); + GC.KeepAlive (this); return result; } } @@ -95,6 +102,7 @@ public SKCodecFrameInfo[] FrameInfo { fixed (SKCodecFrameInfo* i = info) { SkiaApi.sk_codec_get_frame_info (Handle, i); } + GC.KeepAlive (this); return info; } } @@ -103,6 +111,7 @@ public bool GetFrameInfo (int index, out SKCodecFrameInfo frameInfo) { fixed (SKCodecFrameInfo* f = &frameInfo) { var result = SkiaApi.sk_codec_get_frame_info_for_index (Handle, index, f); + GC.KeepAlive (this); return result; } } @@ -152,6 +161,7 @@ public SKCodecResult GetPixels (SKImageInfo info, IntPtr pixels, int rowBytes, S nOptions.fSubset = ⊂ } var result = SkiaApi.sk_codec_get_pixels (Handle, &nInfo, (void*)pixels, (IntPtr)rowBytes, &nOptions); + GC.KeepAlive (this); return result; } @@ -176,6 +186,7 @@ public SKCodecResult StartIncrementalDecode (SKImageInfo info, IntPtr pixels, in } var result = SkiaApi.sk_codec_start_incremental_decode (Handle, &nInfo, (void*)pixels, (IntPtr)rowBytes, &nOptions); + GC.KeepAlive (this); return result; } @@ -183,6 +194,7 @@ public SKCodecResult StartIncrementalDecode (SKImageInfo info, IntPtr pixels, in { var cinfo = SKImageInfoNative.FromManaged (ref info); var result = SkiaApi.sk_codec_start_incremental_decode (Handle, &cinfo, (void*)pixels, (IntPtr)rowBytes, null); + GC.KeepAlive (this); return result; } @@ -192,6 +204,7 @@ public SKCodecResult IncrementalDecode (out int rowsDecoded) { fixed (int* r = &rowsDecoded) { var result = SkiaApi.sk_codec_incremental_decode (Handle, r); + GC.KeepAlive (this); return result; } } @@ -199,6 +212,7 @@ public SKCodecResult IncrementalDecode (out int rowsDecoded) public SKCodecResult IncrementalDecode () { var result = SkiaApi.sk_codec_incremental_decode (Handle, null); + GC.KeepAlive (this); return result; } @@ -220,6 +234,7 @@ public SKCodecResult StartScanlineDecode (SKImageInfo info, SKCodecOptions optio } var result = SkiaApi.sk_codec_start_scanline_decode (Handle, &nInfo, &nOptions); + GC.KeepAlive (this); return result; } @@ -227,6 +242,7 @@ public SKCodecResult StartScanlineDecode (SKImageInfo info) { var cinfo = SKImageInfoNative.FromManaged (ref info); var result = SkiaApi.sk_codec_start_scanline_decode (Handle, &cinfo, null); + GC.KeepAlive (this); return result; } @@ -238,18 +254,21 @@ public int GetScanlines (IntPtr dst, int countLines, int rowBytes) throw new ArgumentNullException (nameof (dst)); var result = SkiaApi.sk_codec_get_scanlines (Handle, (void*)dst, countLines, (IntPtr)rowBytes); + GC.KeepAlive (this); return result; } public bool SkipScanlines (int countLines) { var result = SkiaApi.sk_codec_skip_scanlines (Handle, countLines); + GC.KeepAlive (this); return result; } public SKCodecScanlineOrder ScanlineOrder { get { var result = SkiaApi.sk_codec_get_scanline_order (Handle); + GC.KeepAlive (this); return result; } } @@ -257,6 +276,7 @@ public SKCodecScanlineOrder ScanlineOrder { public int NextScanline { get { var result = SkiaApi.sk_codec_next_scanline (Handle); + GC.KeepAlive (this); return result; } } @@ -264,6 +284,7 @@ public int NextScanline { public int GetOutputScanline (int inputScanline) { var result = SkiaApi.sk_codec_output_scanline (Handle, inputScanline); + GC.KeepAlive (this); return result; } diff --git a/binding/SkiaSharp/SKColorSpace.cs b/binding/SkiaSharp/SKColorSpace.cs index d1982d1b743..8af2b8f1814 100644 --- a/binding/SkiaSharp/SKColorSpace.cs +++ b/binding/SkiaSharp/SKColorSpace.cs @@ -24,11 +24,13 @@ internal SKColorSpace (IntPtr handle, bool owns) void ISKNonVirtualReferenceCounted.ReferenceNative () { SkiaApi.sk_colorspace_ref (Handle); + GC.KeepAlive (this); } void ISKNonVirtualReferenceCounted.UnreferenceNative () { SkiaApi.sk_colorspace_unref (Handle); + GC.KeepAlive (this); } protected override void Dispose (bool disposing) => @@ -39,6 +41,7 @@ protected override void Dispose (bool disposing) => public bool GammaIsCloseToSrgb { get { var result = SkiaApi.sk_colorspace_gamma_close_to_srgb (Handle); + GC.KeepAlive (this); return result; } } @@ -46,6 +49,7 @@ public bool GammaIsCloseToSrgb { public bool GammaIsLinear { get { var result = SkiaApi.sk_colorspace_gamma_is_linear (Handle); + GC.KeepAlive (this); return result; } } @@ -53,6 +57,7 @@ public bool GammaIsLinear { public bool IsSrgb { get { var result = SkiaApi.sk_colorspace_is_srgb (Handle); + GC.KeepAlive (this); return result; } } @@ -138,6 +143,7 @@ public bool GetNumericalTransferFunction (out SKColorSpaceTransferFn fn) { fixed (SKColorSpaceTransferFn* f = &fn) { var result = SkiaApi.sk_colorspace_is_numerical_transfer_fn (Handle, f); + GC.KeepAlive (this); return result; } } @@ -148,6 +154,7 @@ public SKColorSpaceIccProfile ToProfile () { var profile = new SKColorSpaceIccProfile (); SkiaApi.sk_colorspace_to_profile (Handle, profile.Handle); + GC.KeepAlive (this); return profile; } @@ -157,6 +164,7 @@ public bool ToColorSpaceXyz (out SKColorSpaceXyz toXyzD50) { fixed (SKColorSpaceXyz* xyz = &toXyzD50) { var result = SkiaApi.sk_colorspace_to_xyzd50 (Handle, xyz); + GC.KeepAlive (this); return result; } } @@ -169,12 +177,14 @@ public SKColorSpaceXyz ToColorSpaceXyz () => public SKColorSpace ToLinearGamma () { var result = GetObject (SkiaApi.sk_colorspace_make_linear_gamma (Handle)); + GC.KeepAlive (this); return result; } public SKColorSpace ToSrgbGamma () { var result = GetObject (SkiaApi.sk_colorspace_make_srgb_gamma (Handle)); + GC.KeepAlive (this); return result; } diff --git a/binding/SkiaSharp/SKData.cs b/binding/SkiaSharp/SKData.cs index 503e1fba724..6ba80e76711 100644 --- a/binding/SkiaSharp/SKData.cs +++ b/binding/SkiaSharp/SKData.cs @@ -31,11 +31,13 @@ protected override void Dispose (bool disposing) => void ISKNonVirtualReferenceCounted.ReferenceNative () { SkiaApi.sk_data_ref (Handle); + GC.KeepAlive (this); } void ISKNonVirtualReferenceCounted.UnreferenceNative () { SkiaApi.sk_data_unref (Handle); + GC.KeepAlive (this); } public static SKData Empty => @@ -225,6 +227,7 @@ public SKData Subset (ulong offset, ulong length) throw new ArgumentOutOfRangeException (nameof (offset), "The offset exceeds the size of pointers."); } var result = GetObject (SkiaApi.sk_data_new_subset (Handle, (IntPtr)offset, (IntPtr)length)); + GC.KeepAlive (this); return result; } @@ -233,6 +236,7 @@ public SKData Subset (ulong offset, ulong length) public byte[] ToArray () { var array = AsSpan ().ToArray (); + GC.KeepAlive (this); return array; } @@ -243,6 +247,7 @@ public byte[] ToArray () public long Size { get { var result = (long)SkiaApi.sk_data_get_size (Handle); + GC.KeepAlive (this); return result; } } @@ -250,6 +255,7 @@ public long Size { public IntPtr Data { get { var result = (IntPtr)SkiaApi.sk_data_get_data (Handle); + GC.KeepAlive (this); return result; } } @@ -288,6 +294,7 @@ public void SaveTo (Stream target) ptr += copyCount; target.Write ((byte[])buffer, 0, copyCount); } + GC.KeepAlive (this); } internal static SKData GetObject (IntPtr handle) => diff --git a/binding/SkiaSharp/SKDocument.cs b/binding/SkiaSharp/SKDocument.cs index eeb3a50fb4a..e73b05c9428 100644 --- a/binding/SkiaSharp/SKDocument.cs +++ b/binding/SkiaSharp/SKDocument.cs @@ -21,28 +21,33 @@ protected override void Dispose (bool disposing) => public void Abort () { SkiaApi.sk_document_abort (Handle); + GC.KeepAlive (this); } public SKCanvas BeginPage (float width, float height) { var result = OwnedBy (SKCanvas.GetObject (SkiaApi.sk_document_begin_page (Handle, width, height, null), false), this); + GC.KeepAlive (this); return result; } public SKCanvas BeginPage (float width, float height, SKRect content) { var result = OwnedBy (SKCanvas.GetObject (SkiaApi.sk_document_begin_page (Handle, width, height, &content), false), this); + GC.KeepAlive (this); return result; } public void EndPage () { SkiaApi.sk_document_end_page (Handle); + GC.KeepAlive (this); } public void Close () { SkiaApi.sk_document_close (Handle); + GC.KeepAlive (this); } // CreateXps diff --git a/binding/SkiaSharp/SKDrawable.cs b/binding/SkiaSharp/SKDrawable.cs index ef22260e616..2d089be0a7d 100644 --- a/binding/SkiaSharp/SKDrawable.cs +++ b/binding/SkiaSharp/SKDrawable.cs @@ -57,6 +57,7 @@ protected override void DisposeNative () public uint GenerationId { get { var result = SkiaApi.sk_drawable_get_generation_id (Handle); + GC.KeepAlive (this); return result; } } @@ -65,6 +66,7 @@ public SKRect Bounds { get { SKRect bounds; SkiaApi.sk_drawable_get_bounds (Handle, &bounds); + GC.KeepAlive (this); return bounds; } } @@ -72,6 +74,7 @@ public SKRect Bounds { public int ApproximateBytesUsed { get { var result = (int)SkiaApi.sk_drawable_approximate_bytes_used (Handle); + GC.KeepAlive (this); return result; } } @@ -81,6 +84,7 @@ public void Draw (SKCanvas canvas, in SKMatrix matrix) fixed (SKMatrix* m = &matrix) SkiaApi.sk_drawable_draw (Handle, canvas.Handle, m); GC.KeepAlive (canvas); + GC.KeepAlive (this); } public void Draw (SKCanvas canvas, float x, float y) @@ -93,12 +97,14 @@ public void Draw (SKCanvas canvas, float x, float y) public SKPicture Snapshot () { var result = SKPicture.GetObject (SkiaApi.sk_drawable_new_picture_snapshot (Handle), unrefExisting: false); + GC.KeepAlive (this); return result; } public void NotifyDrawingChanged () { SkiaApi.sk_drawable_notify_drawing_changed (Handle); + GC.KeepAlive (this); } protected internal virtual void OnDraw (SKCanvas canvas) diff --git a/binding/SkiaSharp/SKFont.cs b/binding/SkiaSharp/SKFont.cs index c47ffcc3a1d..cd3dc0cb881 100644 --- a/binding/SkiaSharp/SKFont.cs +++ b/binding/SkiaSharp/SKFont.cs @@ -33,126 +33,151 @@ public SKFont (SKTypeface typeface, float size = DefaultSize, float scaleX = Def protected override void DisposeNative () { SkiaApi.sk_font_delete (Handle); + GC.KeepAlive (this); } public bool ForceAutoHinting { get { var r = SkiaApi.sk_font_is_force_auto_hinting (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_font_set_force_auto_hinting (Handle, value); + GC.KeepAlive (this); } } public bool EmbeddedBitmaps { get { var r = SkiaApi.sk_font_is_embedded_bitmaps (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_font_set_embedded_bitmaps (Handle, value); + GC.KeepAlive (this); } } public bool Subpixel { get { var r = SkiaApi.sk_font_is_subpixel (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_font_set_subpixel (Handle, value); + GC.KeepAlive (this); } } public bool LinearMetrics { get { var r = SkiaApi.sk_font_is_linear_metrics (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_font_set_linear_metrics (Handle, value); + GC.KeepAlive (this); } } public bool Embolden { get { var r = SkiaApi.sk_font_is_embolden (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_font_set_embolden (Handle, value); + GC.KeepAlive (this); } } public bool BaselineSnap { get { var r = SkiaApi.sk_font_is_baseline_snap (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_font_set_baseline_snap (Handle, value); + GC.KeepAlive (this); } } public SKFontEdging Edging { get { var r = SkiaApi.sk_font_get_edging (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_font_set_edging (Handle, value); + GC.KeepAlive (this); } } public SKFontHinting Hinting { get { var r = SkiaApi.sk_font_get_hinting (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_font_set_hinting (Handle, value); + GC.KeepAlive (this); } } public SKTypeface Typeface { get { var r = SKTypeface.GetObject (SkiaApi.sk_font_get_typeface (Handle)); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_font_set_typeface (Handle, value == null ? IntPtr.Zero : value.Handle); GC.KeepAlive (value); + GC.KeepAlive (this); } } public float Size { get { var r = SkiaApi.sk_font_get_size (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_font_set_size (Handle, value); + GC.KeepAlive (this); } } public float ScaleX { get { var r = SkiaApi.sk_font_get_scale_x (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_font_set_scale_x (Handle, value); + GC.KeepAlive (this); } } public float SkewX { get { var r = SkiaApi.sk_font_get_skew_x (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_font_set_skew_x (Handle, value); + GC.KeepAlive (this); } } @@ -161,6 +186,7 @@ public float SkewX { public float Spacing { get { var r = SkiaApi.sk_font_get_metrics (Handle, null); + GC.KeepAlive (this); return r; } } @@ -178,6 +204,7 @@ public float GetFontMetrics (out SKFontMetrics metrics) { fixed (SKFontMetrics* m = &metrics) { var r = SkiaApi.sk_font_get_metrics (Handle, m); + GC.KeepAlive (this); return r; } } @@ -187,6 +214,7 @@ public float GetFontMetrics (out SKFontMetrics metrics) public ushort GetGlyph (int codepoint) { var r = SkiaApi.sk_font_unichar_to_glyph (Handle, codepoint); + GC.KeepAlive (this); return r; } @@ -210,6 +238,7 @@ public void GetGlyphs (ReadOnlySpan codepoints, Span glyphs) fixed (int* up = codepoints) fixed (ushort* gp = glyphs) { SkiaApi.sk_font_unichars_to_glyphs (Handle, up, codepoints.Length, gp); + GC.KeepAlive (this); } } @@ -276,6 +305,7 @@ internal void GetGlyphs (void* text, int length, SKTextEncoding encoding, Span glyphs, Span positi fixed (ushort* gp = glyphs) fixed (SKPoint* pp = positions) { SkiaApi.sk_font_get_pos (Handle, gp, glyphs.Length, pp, &origin); + GC.KeepAlive (this); } } @@ -639,6 +673,7 @@ public void GetGlyphOffsets (ReadOnlySpan glyphs, Span offsets, f fixed (ushort* gp = glyphs) fixed (float* pp = offsets) { SkiaApi.sk_font_get_xpos (Handle, gp, glyphs.Length, pp, origin); + GC.KeepAlive (this); } } @@ -784,6 +819,7 @@ public void GetGlyphWidths (ReadOnlySpan glyphs, Span widths, Spa SkiaApi.sk_font_get_widths_bounds (Handle, gp, glyphs.Length, w, b, paint?.Handle ?? IntPtr.Zero); } GC.KeepAlive (paint); + GC.KeepAlive (this); } // GetGlyphPath @@ -795,6 +831,7 @@ public SKPath GetGlyphPath (ushort glyph) path.Dispose (); path = null; } + GC.KeepAlive (this); return path; } @@ -827,6 +864,7 @@ internal SKPath GetTextPath (void* text, int length, SKTextEncoding encoding, SK var path = new SKPath (); SkiaApi.sk_text_utils_get_path (text, (IntPtr)length, encoding, origin.X, origin.Y, Handle, path.Handle); + GC.KeepAlive (this); return path; } @@ -860,6 +898,7 @@ internal SKPath GetTextPath (void* text, int length, SKTextEncoding encoding, Re var path = new SKPath (); fixed (SKPoint* p = positions) { SkiaApi.sk_text_utils_get_pos_path (text, (IntPtr)length, encoding, p, Handle, path.Handle); + GC.KeepAlive (this); } return path; } @@ -873,6 +912,7 @@ public void GetGlyphPaths (ReadOnlySpan glyphs, SKGlyphPathDelegate glyp try { fixed (ushort* g = glyphs) { SkiaApi.sk_font_get_paths (Handle, g, glyphs.Length, proxy, (void*)ctx); + GC.KeepAlive (this); } } finally { gch.Free (); diff --git a/binding/SkiaSharp/SKFontManager.cs b/binding/SkiaSharp/SKFontManager.cs index 08bbfce1cde..1a15f2552c8 100644 --- a/binding/SkiaSharp/SKFontManager.cs +++ b/binding/SkiaSharp/SKFontManager.cs @@ -31,6 +31,7 @@ protected override void Dispose (bool disposing) => public int FontFamilyCount { get { var r = SkiaApi.sk_fontmgr_count_families (Handle); + GC.KeepAlive (this); return r; } } @@ -48,6 +49,7 @@ public string GetFamilyName (int index) { using var str = new SKString (); SkiaApi.sk_fontmgr_get_family_name (Handle, index, str.Handle); + GC.KeepAlive (this); return (string)str; } @@ -56,6 +58,7 @@ public string GetFamilyName (int index) public SKFontStyleSet GetFontStyles (int index) { var styleSet = SKFontStyleSet.GetObject (SkiaApi.sk_fontmgr_create_styleset (Handle, index)); + GC.KeepAlive (this); return styleSet; } @@ -64,6 +67,7 @@ public SKFontStyleSet GetFontStyles (string familyName) var familyNameUtf8ByteList = StringUtilities.GetEncodedText (familyName, SKTextEncoding.Utf8, addNull: true); fixed (byte* familyNamePointer = familyNameUtf8ByteList) { var styleSet = SKFontStyleSet.GetObject (SkiaApi.sk_fontmgr_match_family (Handle, new IntPtr (familyNamePointer))); + GC.KeepAlive (this); return styleSet; } } @@ -79,6 +83,7 @@ public SKTypeface MatchFamily (string familyName, SKFontStyle style) fixed (byte* familyNamePointer = familyNameUtf8ByteList) { var typeface = SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontmgr_match_family_style (Handle, new IntPtr (familyNamePointer), style.Handle)); GC.KeepAlive (style); + GC.KeepAlive (this); return typeface; } } @@ -91,6 +96,7 @@ public SKTypeface CreateTypeface (string path, int index = 0) var utf8path = StringUtilities.GetEncodedText (path, SKTextEncoding.Utf8, true); fixed (byte* u = utf8path) { var typeface = SKTypeface.GetObject (SkiaApi.sk_fontmgr_create_from_file (Handle, u, index)); + GC.KeepAlive (this); return typeface; } } @@ -114,6 +120,7 @@ public SKTypeface CreateTypeface (SKStreamAsset stream, int index = 0) } var typeface = SKTypeface.GetObject (SkiaApi.sk_fontmgr_create_from_stream (Handle, stream.Handle, index)); + GC.KeepAlive (this); stream.RevokeOwnership (typeface); return typeface; } @@ -125,6 +132,7 @@ public SKTypeface CreateTypeface (SKData data, int index = 0) var typeface = SKTypeface.GetObject (SkiaApi.sk_fontmgr_create_from_data (Handle, data.Handle, index)); GC.KeepAlive (data); + GC.KeepAlive (this); return typeface; } @@ -186,6 +194,7 @@ public SKTypeface MatchCharacter (string familyName, SKFontStyle style, string[] fixed (byte* familyNamePointer = familyNameUtf8ByteList) { var typeface = SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontmgr_match_family_style_character (Handle, new IntPtr (familyNamePointer), style.Handle, bcp47, bcp47?.Length ?? 0, character)); GC.KeepAlive (style); + GC.KeepAlive (this); return typeface; } } diff --git a/binding/SkiaSharp/SKFontStyleSet.cs b/binding/SkiaSharp/SKFontStyleSet.cs index 409e5c7a469..9d1a80fc80a 100644 --- a/binding/SkiaSharp/SKFontStyleSet.cs +++ b/binding/SkiaSharp/SKFontStyleSet.cs @@ -24,6 +24,7 @@ protected override void Dispose (bool disposing) => public int Count { get { var r = SkiaApi.sk_fontstyleset_get_count (Handle); + GC.KeepAlive (this); return r; } } @@ -34,6 +35,7 @@ public string GetStyleName (int index) { using var str = new SKString (); SkiaApi.sk_fontstyleset_get_style (Handle, index, IntPtr.Zero, str.Handle); + GC.KeepAlive (this); return (string)str; } @@ -43,6 +45,7 @@ public SKTypeface CreateTypeface (int index) throw new ArgumentOutOfRangeException ($"Index was out of range. Must be non-negative and less than the size of the set.", nameof (index)); var tf = SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontstyleset_create_typeface (Handle, index)); + GC.KeepAlive (this); return tf; } @@ -53,6 +56,7 @@ public SKTypeface CreateTypeface (SKFontStyle style) var tf = SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontstyleset_match_style (Handle, style.Handle)); GC.KeepAlive (style); + GC.KeepAlive (this); return tf; } @@ -72,6 +76,7 @@ private SKFontStyle GetStyle (int index) { var fontStyle = new SKFontStyle (); SkiaApi.sk_fontstyleset_get_style (Handle, index, fontStyle.Handle, IntPtr.Zero); + GC.KeepAlive (this); return fontStyle; } diff --git a/binding/SkiaSharp/SKImage.cs b/binding/SkiaSharp/SKImage.cs index 3ba6ca410df..483166e10a6 100644 --- a/binding/SkiaSharp/SKImage.cs +++ b/binding/SkiaSharp/SKImage.cs @@ -391,6 +391,7 @@ public SKData Encode (SKEncodedImageFormat format, int quality) public int Width { get { var result = SkiaApi.sk_image_get_width (Handle); + GC.KeepAlive (this); return result; } } @@ -398,6 +399,7 @@ public int Width { public int Height { get { var result = SkiaApi.sk_image_get_height (Handle); + GC.KeepAlive (this); return result; } } @@ -405,6 +407,7 @@ public int Height { public uint UniqueId { get { var result = SkiaApi.sk_image_get_unique_id (Handle); + GC.KeepAlive (this); return result; } } @@ -412,6 +415,7 @@ public uint UniqueId { public SKAlphaType AlphaType { get { var result = SkiaApi.sk_image_get_alpha_type (Handle); + GC.KeepAlive (this); return result; } } @@ -419,6 +423,7 @@ public SKAlphaType AlphaType { public SKColorType ColorType { get { var result = SkiaApi.sk_image_get_color_type (Handle).FromNative (); + GC.KeepAlive (this); return result; } } @@ -426,6 +431,7 @@ public SKColorType ColorType { public SKColorSpace ColorSpace { get { var result = SKColorSpace.GetObject (SkiaApi.sk_image_get_colorspace (Handle)); + GC.KeepAlive (this); return result; } } @@ -433,6 +439,7 @@ public SKColorSpace ColorSpace { public bool IsAlphaOnly { get { var result = SkiaApi.sk_image_is_alpha_only (Handle); + GC.KeepAlive (this); return result; } } @@ -440,6 +447,7 @@ public bool IsAlphaOnly { public SKData EncodedData { get { var result = SKData.GetObject (SkiaApi.sk_image_ref_encoded (Handle)); + GC.KeepAlive (this); return result; } } @@ -475,6 +483,7 @@ public SKShader ToShader (SKShaderTileMode tileX, SKShaderTileMode tileY, SKFilt private SKShader ToShader (SKShaderTileMode tileX, SKShaderTileMode tileY, SKSamplingOptions sampling, SKMatrix* localMatrix) { var result = SKShader.GetObject (SkiaApi.sk_image_make_shader (Handle, tileX, tileY, &sampling, localMatrix)); + GC.KeepAlive (this); return result; } @@ -498,6 +507,7 @@ public SKShader ToRawShader (SKShaderTileMode tileX, SKShaderTileMode tileY, SKS private SKShader ToRawShader (SKShaderTileMode tileX, SKShaderTileMode tileY, SKSamplingOptions sampling, SKMatrix* localMatrix) { var result = SKShader.GetObject (SkiaApi.sk_image_make_raw_shader (Handle, tileX, tileY, &sampling, localMatrix)); + GC.KeepAlive (this); return result; } @@ -509,6 +519,7 @@ public bool PeekPixels (SKPixmap pixmap) throw new ArgumentNullException (nameof (pixmap)); var result = SkiaApi.sk_image_peek_pixels (Handle, pixmap.Handle); + GC.KeepAlive (this); GC.KeepAlive (pixmap); if (result) pixmap.pixelSource = this; @@ -528,6 +539,7 @@ public SKPixmap PeekPixels () public bool IsTextureBacked { get { var result = SkiaApi.sk_image_is_texture_backed (Handle); + GC.KeepAlive (this); return result; } } @@ -535,6 +547,7 @@ public bool IsTextureBacked { public bool IsLazyGenerated { get { var result = SkiaApi.sk_image_is_lazy_generated (Handle); + GC.KeepAlive (this); return result; } } @@ -545,6 +558,7 @@ public bool IsValid (GRContext context) => public bool IsValid (GRRecordingContext context) { var result = SkiaApi.sk_image_is_valid (Handle, context?.Handle ?? IntPtr.Zero); + GC.KeepAlive (this); GC.KeepAlive (context); return result; } @@ -564,6 +578,7 @@ public bool ReadPixels (SKImageInfo dstInfo, IntPtr dstPixels, int dstRowBytes, { var cinfo = SKImageInfoNative.FromManaged (ref dstInfo); var result = SkiaApi.sk_image_read_pixels (Handle, &cinfo, (void*)dstPixels, (IntPtr)dstRowBytes, srcX, srcY, cachingHint); + GC.KeepAlive (this); return result; } @@ -579,6 +594,7 @@ public bool ReadPixels (SKPixmap pixmap, int srcX, int srcY, SKImageCachingHint throw new ArgumentNullException (nameof (pixmap)); var result = SkiaApi.sk_image_read_pixels_into_pixmap (Handle, pixmap.Handle, srcX, srcY, cachingHint); + GC.KeepAlive (this); GC.KeepAlive (pixmap); return result; } @@ -603,6 +619,7 @@ public bool ScalePixels (SKPixmap dst, SKSamplingOptions sampling, SKImageCachin if (dst == null) throw new ArgumentNullException (nameof (dst)); var result = SkiaApi.sk_image_scale_pixels (Handle, dst.Handle, &sampling, cachingHint); + GC.KeepAlive (this); GC.KeepAlive (dst); return result; } @@ -612,12 +629,14 @@ public bool ScalePixels (SKPixmap dst, SKSamplingOptions sampling, SKImageCachin public SKImage Subset (SKRectI subset) { var image = GetObject (SkiaApi.sk_image_make_subset_raster (Handle, &subset)); + GC.KeepAlive (this); return image; } public SKImage Subset (GRRecordingContext context, SKRectI subset) { var image = GetObject (SkiaApi.sk_image_make_subset (Handle, context?.Handle ?? IntPtr.Zero, &subset)); + GC.KeepAlive (this); GC.KeepAlive (context); return image; } @@ -632,6 +651,7 @@ public SKImage ToRasterImage (bool ensurePixelData) var image = ensurePixelData ? GetObject (SkiaApi.sk_image_make_raster_image (Handle)) : GetObject (SkiaApi.sk_image_make_non_texture_image (Handle)); + GC.KeepAlive (this); return image; } @@ -649,6 +669,7 @@ public SKImage ToTextureImage (GRContext context, bool mipmapped, bool budgeted) throw new ArgumentNullException (nameof (context)); var image = GetObject (SkiaApi.sk_image_make_texture_image (Handle, context.Handle, mipmapped, budgeted)); + GC.KeepAlive (this); GC.KeepAlive (context); return image; } @@ -670,6 +691,7 @@ public SKImage ApplyImageFilter (SKImageFilter filter, SKRectI subset, SKRectI c fixed (SKRectI* os = &outSubset) fixed (SKPointI* oo = &outOffset) { var image = GetObject (SkiaApi.sk_image_make_with_filter_raster (Handle, filter.Handle, &subset, &clipBounds, os, oo)); + GC.KeepAlive (this); GC.KeepAlive (filter); return image; } @@ -686,6 +708,7 @@ public SKImage ApplyImageFilter (GRRecordingContext context, SKImageFilter filte fixed (SKRectI* os = &outSubset) fixed (SKPointI* oo = &outOffset) { var image = GetObject (SkiaApi.sk_image_make_with_filter (Handle, context?.Handle ?? IntPtr.Zero, filter.Handle, &subset, &clipBounds, os, oo)); + GC.KeepAlive (this); GC.KeepAlive (context); GC.KeepAlive (filter); return image; diff --git a/binding/SkiaSharp/SKNWayCanvas.cs b/binding/SkiaSharp/SKNWayCanvas.cs index df0b8f5f2e6..504716bf392 100644 --- a/binding/SkiaSharp/SKNWayCanvas.cs +++ b/binding/SkiaSharp/SKNWayCanvas.cs @@ -24,6 +24,7 @@ public void AddCanvas (SKCanvas canvas) SkiaApi.sk_nway_canvas_add_canvas (Handle, canvas.Handle); GC.KeepAlive (canvas); + GC.KeepAlive (this); } public void RemoveCanvas (SKCanvas canvas) @@ -33,11 +34,13 @@ public void RemoveCanvas (SKCanvas canvas) SkiaApi.sk_nway_canvas_remove_canvas (Handle, canvas.Handle); GC.KeepAlive (canvas); + GC.KeepAlive (this); } public void RemoveAll () { SkiaApi.sk_nway_canvas_remove_all (Handle); + GC.KeepAlive (this); } } } diff --git a/binding/SkiaSharp/SKPaint.cs b/binding/SkiaSharp/SKPaint.cs index 06887150246..066c11585db 100644 --- a/binding/SkiaSharp/SKPaint.cs +++ b/binding/SkiaSharp/SKPaint.cs @@ -99,6 +99,7 @@ protected override void Dispose (bool disposing) => protected override void DisposeNative () { SkiaApi.sk_compatpaint_delete (Handle); + GC.KeepAlive (this); } // Reset @@ -106,6 +107,7 @@ protected override void DisposeNative () public void Reset () { SkiaApi.sk_compatpaint_reset (Handle, DefaultFont.Handle); + GC.KeepAlive (this); } // properties @@ -113,20 +115,24 @@ public void Reset () public bool IsAntialias { get { var r = SkiaApi.sk_paint_is_antialias (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_compatpaint_set_is_antialias (Handle, value); + GC.KeepAlive (this); } } public bool IsDither { get { var r = SkiaApi.sk_paint_is_dither (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_dither (Handle, value); + GC.KeepAlive (this); } } @@ -146,10 +152,12 @@ public bool SubpixelText { public bool LcdRenderText { get { var r = SkiaApi.sk_compatpaint_get_lcd_render_text (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_compatpaint_set_lcd_render_text (Handle, value); + GC.KeepAlive (this); } } @@ -185,20 +193,24 @@ public bool IsStroke { public SKPaintStyle Style { get { var r = SkiaApi.sk_paint_get_style (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_style (Handle, value); + GC.KeepAlive (this); } } public SKColor Color { get { var r = SkiaApi.sk_paint_get_color (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_color (Handle, (uint)value); + GC.KeepAlive (this); } } @@ -206,10 +218,12 @@ public SKColorF ColorF { get { SKColorF color4f; SkiaApi.sk_paint_get_color4f (Handle, &color4f); + GC.KeepAlive (this); return color4f; } set { SkiaApi.sk_paint_set_color4f (Handle, &value, IntPtr.Zero); + GC.KeepAlive (this); } } @@ -217,110 +231,131 @@ public void SetColor (SKColorF color, SKColorSpace colorspace) { SkiaApi.sk_paint_set_color4f (Handle, &color, colorspace?.Handle ?? IntPtr.Zero); GC.KeepAlive (colorspace); + GC.KeepAlive (this); } public float StrokeWidth { get { var r = SkiaApi.sk_paint_get_stroke_width (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_stroke_width (Handle, value); + GC.KeepAlive (this); } } public float StrokeMiter { get { var r = SkiaApi.sk_paint_get_stroke_miter (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_stroke_miter (Handle, value); + GC.KeepAlive (this); } } public SKStrokeCap StrokeCap { get { var r = SkiaApi.sk_paint_get_stroke_cap (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_stroke_cap (Handle, value); + GC.KeepAlive (this); } } public SKStrokeJoin StrokeJoin { get { var r = SkiaApi.sk_paint_get_stroke_join (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_stroke_join (Handle, value); + GC.KeepAlive (this); } } public SKShader Shader { get { var r = SKShader.GetObject (SkiaApi.sk_paint_get_shader (Handle)); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_shader (Handle, value == null ? IntPtr.Zero : value.Handle); GC.KeepAlive (value); + GC.KeepAlive (this); } } public SKMaskFilter MaskFilter { get { var r = SKMaskFilter.GetObject (SkiaApi.sk_paint_get_maskfilter (Handle)); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_maskfilter (Handle, value == null ? IntPtr.Zero : value.Handle); GC.KeepAlive (value); + GC.KeepAlive (this); } } public SKColorFilter ColorFilter { get { var r = SKColorFilter.GetObject (SkiaApi.sk_paint_get_colorfilter (Handle)); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_colorfilter (Handle, value == null ? IntPtr.Zero : value.Handle); GC.KeepAlive (value); + GC.KeepAlive (this); } } public SKImageFilter ImageFilter { get { var r = SKImageFilter.GetObject (SkiaApi.sk_paint_get_imagefilter (Handle)); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_imagefilter (Handle, value == null ? IntPtr.Zero : value.Handle); GC.KeepAlive (value); + GC.KeepAlive (this); } } public SKBlendMode BlendMode { get { var r = SkiaApi.sk_paint_get_blendmode (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_blendmode (Handle, value); + GC.KeepAlive (this); } } public SKBlender Blender { get { var r = SKBlender.GetObject (SkiaApi.sk_paint_get_blender (Handle)); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_blender (Handle, value == null ? IntPtr.Zero : value.Handle); GC.KeepAlive (value); + GC.KeepAlive (this); } } @@ -328,10 +363,12 @@ public SKBlender Blender { public SKFilterQuality FilterQuality { get { var r = (SKFilterQuality)SkiaApi.sk_compatpaint_get_filter_quality (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_compatpaint_set_filter_quality (Handle, (int)value); + GC.KeepAlive (this); } } @@ -351,10 +388,12 @@ public float TextSize { public SKTextAlign TextAlign { get { var r = SkiaApi.sk_compatpaint_get_text_align (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_compatpaint_set_text_align (Handle, value); + GC.KeepAlive (this); } } @@ -362,10 +401,12 @@ public SKTextAlign TextAlign { public SKTextEncoding TextEncoding { get { var r = SkiaApi.sk_compatpaint_get_text_encoding (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_compatpaint_set_text_encoding (Handle, value); + GC.KeepAlive (this); } } @@ -384,11 +425,13 @@ public float TextSkewX { public SKPathEffect PathEffect { get { var r = SKPathEffect.GetObject (SkiaApi.sk_paint_get_path_effect (Handle)); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_paint_set_path_effect (Handle, value == null ? IntPtr.Zero : value.Handle); GC.KeepAlive (value); + GC.KeepAlive (this); } } @@ -416,6 +459,7 @@ public float GetFontMetrics (out SKFontMetrics metrics) => public SKPaint Clone () { var r = GetObject (SkiaApi.sk_compatpaint_clone (Handle))!; + GC.KeepAlive (this); return r; } @@ -685,6 +729,7 @@ private bool GetFillPath (SKPath src, SKPathBuilder dst, SKRect* cullRect, SKMat var result = SkiaApi.sk_paint_get_fill_path (Handle, src.Handle, dst.Handle, cullRect, &matrix); GC.KeepAlive (src); GC.KeepAlive (dst); + GC.KeepAlive (this); return result; } @@ -983,6 +1028,7 @@ public float[] GetHorizontalTextIntercepts (IntPtr text, IntPtr length, float[] public SKFont ToFont () { var r = SKFont.GetObject (SkiaApi.sk_compatpaint_make_font (Handle)); + GC.KeepAlive (this); return r; } diff --git a/binding/SkiaSharp/SKPath.cs b/binding/SkiaSharp/SKPath.cs index 0e55bebd74c..3f06ec86f7b 100644 --- a/binding/SkiaSharp/SKPath.cs +++ b/binding/SkiaSharp/SKPath.cs @@ -71,10 +71,12 @@ protected override void DisposeNative () public SKPathFillType FillType { get { var r = SkiaApi.sk_path_get_filltype (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_path_set_filltype (Handle, value); + GC.KeepAlive (this); if (_builder != null) _builder.FillType = value; } @@ -110,6 +112,7 @@ public SKPoint LastPoint { get { SKPoint point; SkiaApi.sk_path_get_last_point (Handle, &point); + GC.KeepAlive (this); return point; } } @@ -118,6 +121,7 @@ public SKRect Bounds { get { SKRect rect; SkiaApi.sk_path_get_bounds (Handle, &rect); + GC.KeepAlive (this); return rect; } } @@ -136,6 +140,7 @@ public SKRect GetOvalBounds () { SKRect bounds; var isOval = SkiaApi.sk_path_is_oval (Handle, &bounds); + GC.KeepAlive (this); if (isOval) { return bounds; } else { @@ -147,6 +152,7 @@ public SKRoundRect GetRoundRect () { var rrect = new SKRoundRect (); var result = SkiaApi.sk_path_is_rrect (Handle, rrect.Handle); + GC.KeepAlive (this); if (result) { return rrect; } else { @@ -160,6 +166,7 @@ public SKPoint[] GetLine () var temp = new SKPoint[2]; fixed (SKPoint* t = temp) { var result = SkiaApi.sk_path_is_line (Handle, t); + GC.KeepAlive (this); if (result) { return temp; } else { @@ -177,6 +184,7 @@ public SKRect GetRect (out bool isClosed, out SKPathDirection direction) fixed (SKPathDirection* d = &direction) { SKRect rect; var result = SkiaApi.sk_path_is_rect (Handle, &rect, &c, d); + GC.KeepAlive (this); isClosed = c > 0; if (result) { return rect; @@ -193,6 +201,7 @@ public SKPoint GetPoint (int index) SKPoint point; SkiaApi.sk_path_get_point (Handle, index, &point); + GC.KeepAlive (this); return point; } @@ -207,6 +216,7 @@ public int GetPoints (SKPoint[] points, int max) { fixed (SKPoint* p = points) { var r = SkiaApi.sk_path_get_points (Handle, p, max); + GC.KeepAlive (this); return r; } } @@ -214,6 +224,7 @@ public int GetPoints (SKPoint[] points, int max) public bool Contains (float x, float y) { var r = SkiaApi.sk_path_contains (Handle, x, y); + GC.KeepAlive (this); return r; } @@ -233,6 +244,7 @@ public void Reset () _builder = null; } SkiaApi.sk_path_reset (Handle); + GC.KeepAlive (this); } public bool GetBounds (out SKRect rect) @@ -243,6 +255,7 @@ public bool GetBounds (out SKRect rect) } else { fixed (SKRect* r = &rect) { SkiaApi.sk_path_get_bounds (Handle, r); + GC.KeepAlive (this); } } return !isEmpty; @@ -252,6 +265,7 @@ public SKRect ComputeTightBounds () { SKRect rect; SkiaApi.sk_path_compute_tight_bounds (Handle, &rect); + GC.KeepAlive (this); return rect; } @@ -259,6 +273,7 @@ public void Transform (in SKMatrix matrix) { fixed (SKMatrix* m = &matrix) { SkiaApi.sk_path_transform (Handle, m); + GC.KeepAlive (this); } } @@ -270,6 +285,7 @@ public void Transform (in SKMatrix matrix, SKPath destination) fixed (SKMatrix* m = &matrix) SkiaApi.sk_path_transform_to_dest (Handle, m, destination.Handle); GC.KeepAlive (destination); + GC.KeepAlive (this); } [Obsolete("Use Transform(in SKMatrix) instead.", true)] @@ -300,6 +316,7 @@ public bool Op (SKPath other, SKPathOp op, SKPath result) var success = SkiaApi.sk_pathop_op (Handle, other.Handle, op, result.Handle); GC.KeepAlive (other); GC.KeepAlive (result); + GC.KeepAlive (this); return success; } @@ -321,6 +338,7 @@ public bool Simplify (SKPath result) var success = SkiaApi.sk_pathop_simplify (Handle, result.Handle); GC.KeepAlive (result); + GC.KeepAlive (this); return success; } @@ -339,6 +357,7 @@ public bool GetTightBounds (out SKRect result) { fixed (SKRect* r = &result) { var success = SkiaApi.sk_pathop_tight_bounds (Handle, r); + GC.KeepAlive (this); return success; } } @@ -350,6 +369,7 @@ public bool ToWinding (SKPath result) var success = SkiaApi.sk_pathop_as_winding (Handle, result.Handle); GC.KeepAlive (result); + GC.KeepAlive (this); return success; } @@ -368,6 +388,7 @@ public string ToSvgPathData () { using var str = new SKString (); SkiaApi.sk_path_to_svg_string (Handle, str.Handle); + GC.KeepAlive (this); return (string)str; } @@ -427,6 +448,7 @@ private void FlushBuilder () _builder.Dispose (); _builder = null; SkiaApi.sk_path_delete (Handle); + GC.KeepAlive (this); Handle = newHandle; } @@ -439,6 +461,7 @@ internal void ReplaceFromBuilder (SKPathBuilder builder) var newHandle = SkiaApi.sk_pathbuilder_detach_path (builder.Handle); GC.KeepAlive (builder); SkiaApi.sk_path_delete (Handle); + GC.KeepAlive (this); Handle = newHandle; } @@ -801,6 +824,7 @@ public SKPathVerb Next (Span points) fixed (SKPoint* p = points) { var r = SkiaApi.sk_path_iter_next (Handle, p); + GC.KeepAlive (this); return r; } } @@ -808,18 +832,21 @@ public SKPathVerb Next (Span points) public float ConicWeight () { var r = SkiaApi.sk_path_iter_conic_weight (Handle); + GC.KeepAlive (this); return r; } public bool IsCloseLine () { var r = SkiaApi.sk_path_iter_is_close_line (Handle) != 0; + GC.KeepAlive (this); return r; } public bool IsCloseContour () { var r = SkiaApi.sk_path_iter_is_closed_contour (Handle) != 0; + GC.KeepAlive (this); return r; } } @@ -849,6 +876,7 @@ public SKPathVerb Next (Span points) throw new ArgumentException ("Must be an array of four elements.", nameof (points)); fixed (SKPoint* p = points) { var r = SkiaApi.sk_path_rawiter_next (Handle, p); + GC.KeepAlive (this); return r; } } @@ -856,12 +884,14 @@ public SKPathVerb Next (Span points) public float ConicWeight () { var r = SkiaApi.sk_path_rawiter_conic_weight (Handle); + GC.KeepAlive (this); return r; } public SKPathVerb Peek () { var r = SkiaApi.sk_path_rawiter_peek (Handle); + GC.KeepAlive (this); return r; } } @@ -877,6 +907,7 @@ public void Add (SKPath path, SKPathOp op) { SkiaApi.sk_opbuilder_add (Handle, path.Handle, op); GC.KeepAlive (path); + GC.KeepAlive (this); } public bool Resolve (SKPath result) @@ -886,6 +917,7 @@ public bool Resolve (SKPath result) var success = SkiaApi.sk_opbuilder_resolve (Handle, result.Handle); GC.KeepAlive (result); + GC.KeepAlive (this); return success; } diff --git a/binding/SkiaSharp/SKPathBuilder.cs b/binding/SkiaSharp/SKPathBuilder.cs index 378e48527fa..b33c5c99528 100644 --- a/binding/SkiaSharp/SKPathBuilder.cs +++ b/binding/SkiaSharp/SKPathBuilder.cs @@ -37,28 +37,33 @@ protected override void DisposeNative () => public SKPathFillType FillType { get { var r = SkiaApi.sk_pathbuilder_get_filltype (Handle); + GC.KeepAlive (this); return r; } set { SkiaApi.sk_pathbuilder_set_filltype (Handle, value); + GC.KeepAlive (this); } } public SKPath Detach () { var r = SKPath.GetObject (SkiaApi.sk_pathbuilder_detach_path (Handle)); + GC.KeepAlive (this); return r; } public SKPath Snapshot () { var r = SKPath.GetObject (SkiaApi.sk_pathbuilder_snapshot_path (Handle)); + GC.KeepAlive (this); return r; } public void Reset () { SkiaApi.sk_pathbuilder_reset (Handle); + GC.KeepAlive (this); } // Move @@ -66,21 +71,25 @@ public void Reset () public void MoveTo (SKPoint point) { SkiaApi.sk_pathbuilder_move_to (Handle, point.X, point.Y); + GC.KeepAlive (this); } public void MoveTo (float x, float y) { SkiaApi.sk_pathbuilder_move_to (Handle, x, y); + GC.KeepAlive (this); } public void RMoveTo (SKPoint point) { SkiaApi.sk_pathbuilder_rmove_to (Handle, point.X, point.Y); + GC.KeepAlive (this); } public void RMoveTo (float dx, float dy) { SkiaApi.sk_pathbuilder_rmove_to (Handle, dx, dy); + GC.KeepAlive (this); } // Line @@ -88,21 +97,25 @@ public void RMoveTo (float dx, float dy) public void LineTo (SKPoint point) { SkiaApi.sk_pathbuilder_line_to (Handle, point.X, point.Y); + GC.KeepAlive (this); } public void LineTo (float x, float y) { SkiaApi.sk_pathbuilder_line_to (Handle, x, y); + GC.KeepAlive (this); } public void RLineTo (SKPoint point) { SkiaApi.sk_pathbuilder_rline_to (Handle, point.X, point.Y); + GC.KeepAlive (this); } public void RLineTo (float dx, float dy) { SkiaApi.sk_pathbuilder_rline_to (Handle, dx, dy); + GC.KeepAlive (this); } // Quad @@ -110,21 +123,25 @@ public void RLineTo (float dx, float dy) public void QuadTo (SKPoint point0, SKPoint point1) { SkiaApi.sk_pathbuilder_quad_to (Handle, point0.X, point0.Y, point1.X, point1.Y); + GC.KeepAlive (this); } public void QuadTo (float x0, float y0, float x1, float y1) { SkiaApi.sk_pathbuilder_quad_to (Handle, x0, y0, x1, y1); + GC.KeepAlive (this); } public void RQuadTo (SKPoint point0, SKPoint point1) { SkiaApi.sk_pathbuilder_rquad_to (Handle, point0.X, point0.Y, point1.X, point1.Y); + GC.KeepAlive (this); } public void RQuadTo (float dx0, float dy0, float dx1, float dy1) { SkiaApi.sk_pathbuilder_rquad_to (Handle, dx0, dy0, dx1, dy1); + GC.KeepAlive (this); } // Conic @@ -132,21 +149,25 @@ public void RQuadTo (float dx0, float dy0, float dx1, float dy1) public void ConicTo (SKPoint point0, SKPoint point1, float w) { SkiaApi.sk_pathbuilder_conic_to (Handle, point0.X, point0.Y, point1.X, point1.Y, w); + GC.KeepAlive (this); } public void ConicTo (float x0, float y0, float x1, float y1, float w) { SkiaApi.sk_pathbuilder_conic_to (Handle, x0, y0, x1, y1, w); + GC.KeepAlive (this); } public void RConicTo (SKPoint point0, SKPoint point1, float w) { SkiaApi.sk_pathbuilder_rconic_to (Handle, point0.X, point0.Y, point1.X, point1.Y, w); + GC.KeepAlive (this); } public void RConicTo (float dx0, float dy0, float dx1, float dy1, float w) { SkiaApi.sk_pathbuilder_rconic_to (Handle, dx0, dy0, dx1, dy1, w); + GC.KeepAlive (this); } // Cubic @@ -154,21 +175,25 @@ public void RConicTo (float dx0, float dy0, float dx1, float dy1, float w) public void CubicTo (SKPoint point0, SKPoint point1, SKPoint point2) { SkiaApi.sk_pathbuilder_cubic_to (Handle, point0.X, point0.Y, point1.X, point1.Y, point2.X, point2.Y); + GC.KeepAlive (this); } public void CubicTo (float x0, float y0, float x1, float y1, float x2, float y2) { SkiaApi.sk_pathbuilder_cubic_to (Handle, x0, y0, x1, y1, x2, y2); + GC.KeepAlive (this); } public void RCubicTo (SKPoint point0, SKPoint point1, SKPoint point2) { SkiaApi.sk_pathbuilder_rcubic_to (Handle, point0.X, point0.Y, point1.X, point1.Y, point2.X, point2.Y); + GC.KeepAlive (this); } public void RCubicTo (float dx0, float dy0, float dx1, float dy1, float dx2, float dy2) { SkiaApi.sk_pathbuilder_rcubic_to (Handle, dx0, dy0, dx1, dy1, dx2, dy2); + GC.KeepAlive (this); } // Arc @@ -176,36 +201,43 @@ public void RCubicTo (float dx0, float dy0, float dx1, float dy1, float dx2, flo public void ArcTo (SKPoint r, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, SKPoint xy) { SkiaApi.sk_pathbuilder_arc_to (Handle, r.X, r.Y, xAxisRotate, largeArc, sweep, xy.X, xy.Y); + GC.KeepAlive (this); } public void ArcTo (float rx, float ry, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, float x, float y) { SkiaApi.sk_pathbuilder_arc_to (Handle, rx, ry, xAxisRotate, largeArc, sweep, x, y); + GC.KeepAlive (this); } public void ArcTo (SKRect oval, float startAngle, float sweepAngle, bool forceMoveTo) { SkiaApi.sk_pathbuilder_arc_to_with_oval (Handle, &oval, startAngle, sweepAngle, forceMoveTo); + GC.KeepAlive (this); } public void ArcTo (SKPoint point1, SKPoint point2, float radius) { SkiaApi.sk_pathbuilder_arc_to_with_points (Handle, point1.X, point1.Y, point2.X, point2.Y, radius); + GC.KeepAlive (this); } public void ArcTo (float x1, float y1, float x2, float y2, float radius) { SkiaApi.sk_pathbuilder_arc_to_with_points (Handle, x1, y1, x2, y2, radius); + GC.KeepAlive (this); } public void RArcTo (SKPoint r, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, SKPoint xy) { SkiaApi.sk_pathbuilder_rarc_to (Handle, r.X, r.Y, xAxisRotate, largeArc, sweep, xy.X, xy.Y); + GC.KeepAlive (this); } public void RArcTo (float rx, float ry, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, float x, float y) { SkiaApi.sk_pathbuilder_rarc_to (Handle, rx, ry, xAxisRotate, largeArc, sweep, x, y); + GC.KeepAlive (this); } // Close @@ -213,6 +245,7 @@ public void RArcTo (float rx, float ry, float xAxisRotate, SKPathArcSize largeAr public void Close () { SkiaApi.sk_pathbuilder_close (Handle); + GC.KeepAlive (this); } // Add shapes @@ -220,6 +253,7 @@ public void Close () public void AddRect (SKRect rect, SKPathDirection direction = SKPathDirection.Clockwise) { SkiaApi.sk_pathbuilder_add_rect (Handle, &rect, direction); + GC.KeepAlive (this); } public void AddRect (SKRect rect, SKPathDirection direction, uint startIndex) @@ -228,6 +262,7 @@ public void AddRect (SKRect rect, SKPathDirection direction, uint startIndex) throw new ArgumentOutOfRangeException (nameof (startIndex), "Starting index must be in the range of 0..3 (inclusive)."); SkiaApi.sk_pathbuilder_add_rect_start (Handle, &rect, direction, startIndex); + GC.KeepAlive (this); } public void AddRoundRect (SKRoundRect rect, SKPathDirection direction = SKPathDirection.Clockwise) @@ -236,6 +271,7 @@ public void AddRoundRect (SKRoundRect rect, SKPathDirection direction = SKPathDi throw new ArgumentNullException (nameof (rect)); SkiaApi.sk_pathbuilder_add_rrect (Handle, rect.Handle, direction); GC.KeepAlive (rect); + GC.KeepAlive (this); } public void AddRoundRect (SKRoundRect rect, SKPathDirection direction, uint startIndex) @@ -244,32 +280,38 @@ public void AddRoundRect (SKRoundRect rect, SKPathDirection direction, uint star throw new ArgumentNullException (nameof (rect)); SkiaApi.sk_pathbuilder_add_rrect_start (Handle, rect.Handle, direction, startIndex); GC.KeepAlive (rect); + GC.KeepAlive (this); } public void AddOval (SKRect rect, SKPathDirection direction = SKPathDirection.Clockwise) { SkiaApi.sk_pathbuilder_add_oval (Handle, &rect, direction); + GC.KeepAlive (this); } public void AddArc (SKRect oval, float startAngle, float sweepAngle) { SkiaApi.sk_pathbuilder_add_arc (Handle, &oval, startAngle, sweepAngle); + GC.KeepAlive (this); } public void AddRoundRect (SKRect rect, float rx, float ry, SKPathDirection dir = SKPathDirection.Clockwise) { SkiaApi.sk_pathbuilder_add_rounded_rect (Handle, &rect, rx, ry, dir); + GC.KeepAlive (this); } public void AddCircle (float x, float y, float radius, SKPathDirection dir = SKPathDirection.Clockwise) { SkiaApi.sk_pathbuilder_add_circle (Handle, x, y, radius, dir); + GC.KeepAlive (this); } public void AddPoly (ReadOnlySpan points, bool close = true) { fixed (SKPoint* p = points) { SkiaApi.sk_pathbuilder_add_poly (Handle, p, points.Length, close); + GC.KeepAlive (this); } } @@ -279,6 +321,7 @@ public void AddPoly (SKPoint[] points, bool close = true) throw new ArgumentNullException (nameof (points)); fixed (SKPoint* p = points) { SkiaApi.sk_pathbuilder_add_poly (Handle, p, points.Length, close); + GC.KeepAlive (this); } } @@ -291,6 +334,7 @@ public void AddPath (SKPath other, float dx, float dy, SKPathAddMode mode = SKPa SkiaApi.sk_pathbuilder_add_path_offset (Handle, other.Handle, dx, dy, mode); GC.KeepAlive (other); + GC.KeepAlive (this); } public void AddPath (SKPath other, in SKMatrix matrix, SKPathAddMode mode = SKPathAddMode.Append) @@ -301,6 +345,7 @@ public void AddPath (SKPath other, in SKMatrix matrix, SKPathAddMode mode = SKPa fixed (SKMatrix* m = &matrix) SkiaApi.sk_pathbuilder_add_path_matrix (Handle, other.Handle, m, mode); GC.KeepAlive (other); + GC.KeepAlive (this); } public void AddPath (SKPath other, SKPathAddMode mode = SKPathAddMode.Append) @@ -310,6 +355,7 @@ public void AddPath (SKPath other, SKPathAddMode mode = SKPathAddMode.Append) SkiaApi.sk_pathbuilder_add_path (Handle, other.Handle, mode); GC.KeepAlive (other); + GC.KeepAlive (this); } public void ReverseAddPath (SKPath other) @@ -319,6 +365,7 @@ public void ReverseAddPath (SKPath other) SkiaApi.sk_pathbuilder_reverse_add_path (Handle, other.Handle); GC.KeepAlive (other); + GC.KeepAlive (this); } } } diff --git a/binding/SkiaSharp/SKPathMeasure.cs b/binding/SkiaSharp/SKPathMeasure.cs index 9f6986faf6c..5a03e8980f6 100644 --- a/binding/SkiaSharp/SKPathMeasure.cs +++ b/binding/SkiaSharp/SKPathMeasure.cs @@ -44,6 +44,7 @@ protected override void DisposeNative () => public float Length { get { var r = SkiaApi.sk_pathmeasure_get_length (Handle); + GC.KeepAlive (this); return r; } } @@ -51,6 +52,7 @@ public float Length { public bool IsClosed { get { var r = SkiaApi.sk_pathmeasure_is_closed (Handle); + GC.KeepAlive (this); return r; } } @@ -64,6 +66,7 @@ public void SetPath (SKPath path, bool forceClosed) { SkiaApi.sk_pathmeasure_set_path (Handle, path == null ? IntPtr.Zero : path.Handle, forceClosed); GC.KeepAlive (path); + GC.KeepAlive (this); } // GetPositionAndTangent @@ -73,6 +76,7 @@ public bool GetPositionAndTangent (float distance, out SKPoint position, out SKP fixed (SKPoint* p = &position) fixed (SKPoint* t = &tangent) { var r = SkiaApi.sk_pathmeasure_get_pos_tan (Handle, distance, p, t); + GC.KeepAlive (this); return r; } } @@ -90,6 +94,7 @@ public bool GetPosition (float distance, out SKPoint position) { fixed (SKPoint* p = &position) { var r = SkiaApi.sk_pathmeasure_get_pos_tan (Handle, distance, p, null); + GC.KeepAlive (this); return r; } } @@ -107,6 +112,7 @@ public bool GetTangent (float distance, out SKPoint tangent) { fixed (SKPoint* t = &tangent) { var r = SkiaApi.sk_pathmeasure_get_pos_tan (Handle, distance, null, t); + GC.KeepAlive (this); return r; } } @@ -124,6 +130,7 @@ public bool GetMatrix (float distance, out SKMatrix matrix, SKPathMeasureMatrixF { fixed (SKMatrix* m = &matrix) { var r = SkiaApi.sk_pathmeasure_get_matrix (Handle, distance, m, flags); + GC.KeepAlive (this); return r; } } @@ -136,6 +143,7 @@ public bool GetSegment (float start, float stop, SKPathBuilder dst, bool startWi throw new ArgumentNullException (nameof (dst)); var result = SkiaApi.sk_pathmeasure_get_segment (Handle, start, stop, dst.Handle, startWithMoveTo); GC.KeepAlive (dst); + GC.KeepAlive (this); return result; } @@ -167,6 +175,7 @@ public SKPath GetSegment (float start, float stop, bool startWithMoveTo) public bool NextContour () { var r = SkiaApi.sk_pathmeasure_next_contour (Handle); + GC.KeepAlive (this); return r; } } diff --git a/binding/SkiaSharp/SKPicture.cs b/binding/SkiaSharp/SKPicture.cs index 243feb3493a..3ccdb97eeab 100644 --- a/binding/SkiaSharp/SKPicture.cs +++ b/binding/SkiaSharp/SKPicture.cs @@ -18,6 +18,7 @@ protected override void Dispose (bool disposing) => public uint UniqueId { get { var result = SkiaApi.sk_picture_get_unique_id (Handle); + GC.KeepAlive (this); return result; } } @@ -26,6 +27,7 @@ public SKRect CullRect { get { SKRect rect; SkiaApi.sk_picture_get_cull_rect (Handle, &rect); + GC.KeepAlive (this); return rect; } } @@ -33,6 +35,7 @@ public SKRect CullRect { public int ApproximateBytesUsed { get { var result = (int)SkiaApi.sk_picture_approximate_bytes_used (Handle); + GC.KeepAlive (this); return result; } } @@ -43,6 +46,7 @@ public int ApproximateBytesUsed { public int GetApproximateOperationCount(bool includeNested) { var result = SkiaApi.sk_picture_approximate_op_count (Handle, includeNested); + GC.KeepAlive (this); return result; } @@ -51,6 +55,7 @@ public int GetApproximateOperationCount(bool includeNested) public SKData Serialize () { var result = SKData.GetObject (SkiaApi.sk_picture_serialize_to_data (Handle)); + GC.KeepAlive (this); return result; } @@ -70,6 +75,7 @@ public void Serialize (SKWStream stream) SkiaApi.sk_picture_serialize_to_stream (Handle, stream.Handle); GC.KeepAlive (stream); + GC.KeepAlive (this); } // Playback @@ -81,6 +87,7 @@ public void Playback (SKCanvas canvas) SkiaApi.sk_picture_playback (Handle, canvas.Handle); GC.KeepAlive (canvas); + GC.KeepAlive (this); } // ToShader @@ -109,6 +116,7 @@ public SKShader ToShader (SKShaderTileMode tmx, SKShaderTileMode tmy, SKFilterMo private SKShader ToShader (SKShaderTileMode tmx, SKShaderTileMode tmy, SKFilterMode filterMode, SKMatrix* localMatrix, SKRect* tile) { var result = SKShader.GetObject (SkiaApi.sk_picture_make_shader (Handle, tmx, tmy, filterMode, localMatrix, tile)); + GC.KeepAlive (this); return result; } diff --git a/binding/SkiaSharp/SKPictureRecorder.cs b/binding/SkiaSharp/SKPictureRecorder.cs index efe1aa6543a..c33ce303914 100644 --- a/binding/SkiaSharp/SKPictureRecorder.cs +++ b/binding/SkiaSharp/SKPictureRecorder.cs @@ -28,6 +28,7 @@ protected override void DisposeNative () => public SKCanvas BeginRecording (SKRect cullRect) { var result = OwnedBy (SKCanvas.GetObject (SkiaApi.sk_picture_recorder_begin_recording (Handle, &cullRect), false), this); + GC.KeepAlive (this); return result; } @@ -43,6 +44,7 @@ public SKCanvas BeginRecording (SKRect cullRect, bool useRTree) try { rtreeHandle = SkiaApi.sk_rtree_factory_new (); var result = OwnedBy (SKCanvas.GetObject (SkiaApi.sk_picture_recorder_begin_recording_with_bbh_factory (Handle, &cullRect, rtreeHandle), false), this); + GC.KeepAlive (this); return result; } finally { if (rtreeHandle != IntPtr.Zero) { @@ -54,18 +56,21 @@ public SKCanvas BeginRecording (SKRect cullRect, bool useRTree) public SKPicture EndRecording () { var result = SKPicture.GetObject (SkiaApi.sk_picture_recorder_end_recording (Handle)); + GC.KeepAlive (this); return result; } public SKDrawable EndRecordingAsDrawable () { var result = SKDrawable.GetObject (SkiaApi.sk_picture_recorder_end_recording_as_drawable (Handle)); + GC.KeepAlive (this); return result; } public SKCanvas RecordingCanvas { get { var result = OwnedBy (SKCanvas.GetObject (SkiaApi.sk_picture_get_recording_canvas (Handle), false), this); + GC.KeepAlive (this); return result; } } diff --git a/binding/SkiaSharp/SKPixmap.cs b/binding/SkiaSharp/SKPixmap.cs index 65b35f475c6..587213d9473 100644 --- a/binding/SkiaSharp/SKPixmap.cs +++ b/binding/SkiaSharp/SKPixmap.cs @@ -56,6 +56,7 @@ protected override void DisposeManaged () public void Reset () { SkiaApi.sk_pixmap_reset (Handle); + GC.KeepAlive (this); pixelSource = null; } @@ -63,6 +64,7 @@ public void Reset (SKImageInfo info, IntPtr addr, int rowBytes) { var cinfo = SKImageInfoNative.FromManaged (ref info); SkiaApi.sk_pixmap_reset_with_params (Handle, &cinfo, (void*)addr, (IntPtr)rowBytes); + GC.KeepAlive (this); pixelSource = null; } @@ -72,6 +74,7 @@ public SKImageInfo Info { get { SKImageInfoNative cinfo; SkiaApi.sk_pixmap_get_info (Handle, &cinfo); + GC.KeepAlive (this); return SKImageInfoNative.ToManaged (ref cinfo); } } @@ -96,6 +99,7 @@ public SKSizeI Size { public SKColorSpace? ColorSpace { get { var result = SKColorSpace.GetObject (SkiaApi.sk_pixmap_get_colorspace (Handle)); + GC.KeepAlive (this); return result; } } @@ -107,6 +111,7 @@ public SKColorSpace? ColorSpace { public int RowBytes { get { var result = (int)SkiaApi.sk_pixmap_get_row_bytes (Handle); + GC.KeepAlive (this); return result; } } @@ -120,12 +125,14 @@ public int RowBytes { public IntPtr GetPixels () { var result = (IntPtr)SkiaApi.sk_pixmap_get_writable_addr (Handle); + GC.KeepAlive (this); return result; } public IntPtr GetPixels (int x, int y) { var result = (IntPtr)SkiaApi.sk_pixmap_get_writeable_addr_with_xy (Handle, x, y); + GC.KeepAlive (this); return result; } @@ -178,6 +185,7 @@ public unsafe Span GetPixelSpan (int x, int y) } var addr = SkiaApi.sk_pixmap_get_writable_addr (Handle); + GC.KeepAlive (this); var span = new Span (addr, spanLength); if (spanOffset != 0) @@ -189,6 +197,7 @@ public unsafe Span GetPixelSpan (int x, int y) public SKColor GetPixelColor (int x, int y) { var result = SkiaApi.sk_pixmap_get_pixel_color (Handle, x, y); + GC.KeepAlive (this); return result; } @@ -196,12 +205,14 @@ public SKColorF GetPixelColorF (int x, int y) { SKColorF color; SkiaApi.sk_pixmap_get_pixel_color4f (Handle, x, y, &color); + GC.KeepAlive (this); return color; } public float GetPixelAlpha (int x, int y) { var result = SkiaApi.sk_pixmap_get_pixel_alphaf (Handle, x, y); + GC.KeepAlive (this); return result; } @@ -218,6 +229,7 @@ public bool ScalePixels (SKPixmap destination, SKSamplingOptions sampling) { _ = destination ?? throw new ArgumentNullException (nameof (destination)); var result = SkiaApi.sk_pixmap_scale_pixels (Handle, destination.Handle, &sampling); + GC.KeepAlive (this); GC.KeepAlive (destination); return result; } @@ -228,6 +240,7 @@ public bool ReadPixels (SKImageInfo dstInfo, IntPtr dstPixels, int dstRowBytes, { var cinfo = SKImageInfoNative.FromManaged (ref dstInfo); var result = SkiaApi.sk_pixmap_read_pixels (Handle, &cinfo, (void*)dstPixels, (IntPtr)dstRowBytes, srcX, srcY); + GC.KeepAlive (this); return result; } @@ -289,6 +302,7 @@ public bool Encode (SKWStream dst, SKWebpEncoderOptions options) { _ = dst ?? throw new ArgumentNullException (nameof (dst)); var result = SkiaApi.sk_webpencoder_encode (dst.Handle, Handle, &options); + GC.KeepAlive (this); GC.KeepAlive (dst); return result; } @@ -313,6 +327,7 @@ public bool Encode (SKWStream dst, SKJpegEncoderOptions options) { _ = dst ?? throw new ArgumentNullException (nameof (dst)); var result = SkiaApi.sk_jpegencoder_encode (dst.Handle, Handle, &options); + GC.KeepAlive (this); GC.KeepAlive (dst); return result; } @@ -337,6 +352,7 @@ public bool Encode (SKWStream dst, SKPngEncoderOptions options) { _ = dst ?? throw new ArgumentNullException (nameof (dst)); var result = SkiaApi.sk_pngencoder_encode (dst.Handle, Handle, &options); + GC.KeepAlive (this); GC.KeepAlive (dst); return result; } @@ -357,6 +373,7 @@ public bool ExtractSubset (SKPixmap result, SKRectI subset) { _ = result ?? throw new ArgumentNullException (nameof (result)); var extracted = SkiaApi.sk_pixmap_extract_subset (Handle, result.Handle, &subset); + GC.KeepAlive (this); GC.KeepAlive (result); return extracted; } @@ -369,6 +386,7 @@ public bool Erase (SKColor color) => public bool Erase (SKColor color, SKRectI subset) { var result = SkiaApi.sk_pixmap_erase_color (Handle, (uint)color, &subset); + GC.KeepAlive (this); return result; } @@ -378,6 +396,7 @@ public bool Erase (SKColorF color) => public bool Erase (SKColorF color, SKRectI subset) { var result = SkiaApi.sk_pixmap_erase_color4f (Handle, &color, &subset); + GC.KeepAlive (this); return result; } @@ -386,6 +405,7 @@ public bool Erase (SKColorF color, SKRectI subset) public bool ComputeIsOpaque () { var result = SkiaApi.sk_pixmap_compute_is_opaque (Handle); + GC.KeepAlive (this); return result; } diff --git a/binding/SkiaSharp/SKRegion.cs b/binding/SkiaSharp/SKRegion.cs index 32f4cbed98c..0c1d4457cc9 100644 --- a/binding/SkiaSharp/SKRegion.cs +++ b/binding/SkiaSharp/SKRegion.cs @@ -45,6 +45,7 @@ protected override void DisposeNative () => public bool IsEmpty { get { var result = SkiaApi.sk_region_is_empty (Handle); + GC.KeepAlive (this); return result; } } @@ -52,6 +53,7 @@ public bool IsEmpty { public bool IsRect { get { var result = SkiaApi.sk_region_is_rect (Handle); + GC.KeepAlive (this); return result; } } @@ -59,6 +61,7 @@ public bool IsRect { public bool IsComplex { get { var result = SkiaApi.sk_region_is_complex (Handle); + GC.KeepAlive (this); return result; } } @@ -67,6 +70,7 @@ public SKRectI Bounds { get { SKRectI rect; SkiaApi.sk_region_get_bounds (Handle, &rect); + GC.KeepAlive (this); return rect; } } @@ -80,6 +84,7 @@ public SKPath GetBoundaryPath () path.Dispose (); path = null; } + GC.KeepAlive (this); return path; } @@ -101,24 +106,28 @@ public bool Contains (SKRegion src) var result = SkiaApi.sk_region_contains (Handle, src.Handle); GC.KeepAlive (src); + GC.KeepAlive (this); return result; } public bool Contains (SKPointI xy) { var result = SkiaApi.sk_region_contains_point (Handle, xy.X, xy.Y); + GC.KeepAlive (this); return result; } public bool Contains (int x, int y) { var result = SkiaApi.sk_region_contains_point (Handle, x, y); + GC.KeepAlive (this); return result; } public bool Contains (SKRectI rect) { var result = SkiaApi.sk_region_contains_rect (Handle, &rect); + GC.KeepAlive (this); return result; } @@ -127,6 +136,7 @@ public bool Contains (SKRectI rect) public bool QuickContains (SKRectI rect) { var result = SkiaApi.sk_region_quick_contains (Handle, &rect); + GC.KeepAlive (this); return result; } @@ -135,6 +145,7 @@ public bool QuickContains (SKRectI rect) public bool QuickReject (SKRectI rect) { var result = SkiaApi.sk_region_quick_reject_rect (Handle, &rect); + GC.KeepAlive (this); return result; } @@ -145,6 +156,7 @@ public bool QuickReject (SKRegion region) var result = SkiaApi.sk_region_quick_reject (Handle, region.Handle); GC.KeepAlive (region); + GC.KeepAlive (this); return result; } @@ -175,12 +187,14 @@ public bool Intersects (SKRegion region) var result = SkiaApi.sk_region_intersects (Handle, region.Handle); GC.KeepAlive (region); + GC.KeepAlive (this); return result; } public bool Intersects (SKRectI rect) { var result = SkiaApi.sk_region_intersects_rect (Handle, &rect); + GC.KeepAlive (this); return result; } @@ -189,6 +203,7 @@ public bool Intersects (SKRectI rect) public void SetEmpty () { SkiaApi.sk_region_set_empty (Handle); + GC.KeepAlive (this); } public bool SetRegion (SKRegion region) @@ -198,12 +213,14 @@ public bool SetRegion (SKRegion region) var result = SkiaApi.sk_region_set_region (Handle, region.Handle); GC.KeepAlive (region); + GC.KeepAlive (this); return result; } public bool SetRect (SKRectI rect) { var result = SkiaApi.sk_region_set_rect (Handle, &rect); + GC.KeepAlive (this); return result; } @@ -211,6 +228,7 @@ public bool SetRects (ReadOnlySpan rects) { fixed (SKRectI* r = rects) { var result = SkiaApi.sk_region_set_rects (Handle, r, rects.Length); + GC.KeepAlive (this); return result; } } @@ -225,6 +243,7 @@ public bool SetPath (SKPath path, SKRegion clip) var result = SkiaApi.sk_region_set_path (Handle, path.Handle, clip.Handle); GC.KeepAlive (path); GC.KeepAlive (clip); + GC.KeepAlive (this); return result; } @@ -240,6 +259,7 @@ public bool SetPath (SKPath path) var result = SkiaApi.sk_region_set_path (Handle, path.Handle, clip.Handle); GC.KeepAlive (path); + GC.KeepAlive (this); return result; } @@ -248,6 +268,7 @@ public bool SetPath (SKPath path) public void Translate (int x, int y) { SkiaApi.sk_region_translate (Handle, x, y); + GC.KeepAlive (this); } // Op @@ -255,6 +276,7 @@ public void Translate (int x, int y) public bool Op (SKRectI rect, SKRegionOperation op) { var result = SkiaApi.sk_region_op_rect (Handle, &rect, op); + GC.KeepAlive (this); return result; } @@ -265,6 +287,7 @@ public bool Op (SKRegion region, SKRegionOperation op) { var result = SkiaApi.sk_region_op (Handle, region.Handle, op); GC.KeepAlive (region); + GC.KeepAlive (this); return result; } @@ -304,6 +327,7 @@ public bool Next (out SKRectI rect) { if (SkiaApi.sk_region_iterator_done (Handle)) { rect = SKRectI.Empty; + GC.KeepAlive (this); return false; } @@ -312,6 +336,7 @@ public bool Next (out SKRectI rect) } SkiaApi.sk_region_iterator_next (Handle); + GC.KeepAlive (this); return true; } @@ -336,6 +361,7 @@ public bool Next (out SKRectI rect) { if (SkiaApi.sk_region_cliperator_done (Handle)) { rect = SKRectI.Empty; + GC.KeepAlive (this); return false; } @@ -344,6 +370,7 @@ public bool Next (out SKRectI rect) } SkiaApi.sk_region_cliperator_next (Handle); + GC.KeepAlive (this); return true; } @@ -366,11 +393,13 @@ public bool Next (out int left, out int right) if (SkiaApi.sk_region_spanerator_next (Handle, &l, &r)) { left = l; right = r; + GC.KeepAlive (this); return true; } left = 0; right = 0; + GC.KeepAlive (this); return false; } } diff --git a/binding/SkiaSharp/SKRoundRect.cs b/binding/SkiaSharp/SKRoundRect.cs index f1e302ce90a..05412b91a77 100644 --- a/binding/SkiaSharp/SKRoundRect.cs +++ b/binding/SkiaSharp/SKRoundRect.cs @@ -59,6 +59,7 @@ public SKRect Rect { get { SKRect rect; SkiaApi.sk_rrect_get_rect (Handle, &rect); + GC.KeepAlive (this); return rect; } } @@ -73,6 +74,7 @@ public SKRect Rect { public SKRoundRectType Type { get { var r = SkiaApi.sk_rrect_get_type (Handle); + GC.KeepAlive (this); return r; } } @@ -80,6 +82,7 @@ public SKRoundRectType Type { public float Width { get { var r = SkiaApi.sk_rrect_get_width (Handle); + GC.KeepAlive (this); return r; } } @@ -87,6 +90,7 @@ public float Width { public float Height { get { var r = SkiaApi.sk_rrect_get_height (Handle); + GC.KeepAlive (this); return r; } } @@ -94,6 +98,7 @@ public float Height { public bool IsValid { get { var r = SkiaApi.sk_rrect_is_valid (Handle); + GC.KeepAlive (this); return r; } } @@ -117,26 +122,31 @@ public bool CheckAllCornersCircular (float tolerance) public void SetEmpty () { SkiaApi.sk_rrect_set_empty (Handle); + GC.KeepAlive (this); } public void SetRect (SKRect rect) { SkiaApi.sk_rrect_set_rect (Handle, &rect); + GC.KeepAlive (this); } public void SetRect (SKRect rect, float xRadius, float yRadius) { SkiaApi.sk_rrect_set_rect_xy (Handle, &rect, xRadius, yRadius); + GC.KeepAlive (this); } public void SetOval (SKRect rect) { SkiaApi.sk_rrect_set_oval (Handle, &rect); + GC.KeepAlive (this); } public void SetNinePatch (SKRect rect, float leftRadius, float topRadius, float rightRadius, float bottomRadius) { SkiaApi.sk_rrect_set_nine_patch (Handle, &rect, leftRadius, topRadius, rightRadius, bottomRadius); + GC.KeepAlive (this); } public void SetRectRadii (SKRect rect, SKPoint[] radii) @@ -154,12 +164,14 @@ public void SetRectRadii (SKRect rect, ReadOnlySpan radii) fixed (SKPoint* r = radii) { SkiaApi.sk_rrect_set_rect_radii (Handle, &rect, r); + GC.KeepAlive (this); } } public bool Contains (SKRect rect) { var r = SkiaApi.sk_rrect_contains (Handle, &rect); + GC.KeepAlive (this); return r; } @@ -167,6 +179,7 @@ public SKPoint GetRadii (SKRoundRectCorner corner) { SKPoint radii; SkiaApi.sk_rrect_get_radii (Handle, corner, &radii); + GC.KeepAlive (this); return radii; } @@ -178,6 +191,7 @@ public void Deflate (SKSize size) public void Deflate (float dx, float dy) { SkiaApi.sk_rrect_inset (Handle, dx, dy); + GC.KeepAlive (this); } public void Inflate (SKSize size) @@ -188,6 +202,7 @@ public void Inflate (SKSize size) public void Inflate (float dx, float dy) { SkiaApi.sk_rrect_outset (Handle, dx, dy); + GC.KeepAlive (this); } public void Offset (SKPoint pos) @@ -198,12 +213,14 @@ public void Offset (SKPoint pos) public void Offset (float dx, float dy) { SkiaApi.sk_rrect_offset (Handle, dx, dy); + GC.KeepAlive (this); } public bool TryTransform (SKMatrix matrix, out SKRoundRect transformed) { var destHandle = SkiaApi.sk_rrect_new (); var success = SkiaApi.sk_rrect_transform (Handle, &matrix, destHandle); + GC.KeepAlive (this); if (success) { transformed = new SKRoundRect (destHandle, true); return true; diff --git a/binding/SkiaSharp/SKRuntimeEffect.cs b/binding/SkiaSharp/SKRuntimeEffect.cs index 84df6d7a8d8..b98a010ecce 100644 --- a/binding/SkiaSharp/SKRuntimeEffect.cs +++ b/binding/SkiaSharp/SKRuntimeEffect.cs @@ -90,6 +90,7 @@ private static void ValidateResult (SKRuntimeEffect effect, string errors) public int UniformSize { get { var size = (int)SkiaApi.sk_runtimeeffect_get_uniform_byte_size (Handle); + GC.KeepAlive (this); return size; } } @@ -110,6 +111,7 @@ private IEnumerable GetChildrenNames () SkiaApi.sk_runtimeeffect_get_child_name (Handle, i, str.Handle); yield return str.ToString (); } + GC.KeepAlive (this); } private IEnumerable GetUniformNames () @@ -120,6 +122,7 @@ private IEnumerable GetUniformNames () SkiaApi.sk_runtimeeffect_get_uniform_name (Handle, i, str.Handle); yield return str.ToString (); } + GC.KeepAlive (this); } // ToShader @@ -144,6 +147,7 @@ private SKShader ToShader (SKData uniforms, SKObject[] children, SKMatrix* local fixed (IntPtr* ch = childrenHandles) { var shader = SKShader.GetObject (SkiaApi.sk_runtimeeffect_make_shader (Handle, uniformsHandle, ch, (IntPtr)childrenHandles.Length, localMatrix)); GC.KeepAlive (uniforms); + GC.KeepAlive (this); return shader; } } @@ -170,6 +174,7 @@ private SKColorFilter ToColorFilter (SKData uniforms, SKObject[] children) fixed (IntPtr* ch = childrenHandles) { var colorFilter = SKColorFilter.GetObject (SkiaApi.sk_runtimeeffect_make_color_filter (Handle, uniformsHandle, ch, (IntPtr)childrenHandles.Length)); GC.KeepAlive (uniforms); + GC.KeepAlive (this); return colorFilter; } } @@ -196,6 +201,7 @@ private SKBlender ToBlender (SKData uniforms, SKObject[] children) fixed (IntPtr* ch = childrenHandles) { var blender = SKBlender.GetObject (SkiaApi.sk_runtimeeffect_make_blender (Handle, uniformsHandle, ch, (IntPtr)childrenHandles.Length)); GC.KeepAlive (uniforms); + GC.KeepAlive (this); return blender; } } diff --git a/binding/SkiaSharp/SKShader.cs b/binding/SkiaSharp/SKShader.cs index cb88d58e1e5..85566572135 100644 --- a/binding/SkiaSharp/SKShader.cs +++ b/binding/SkiaSharp/SKShader.cs @@ -24,6 +24,7 @@ public SKShader WithColorFilter (SKColorFilter filter) var shader = GetObject (SkiaApi.sk_shader_with_color_filter (Handle, filter.Handle)); GC.KeepAlive (filter); + GC.KeepAlive (this); return shader; } @@ -32,6 +33,7 @@ public SKShader WithColorFilter (SKColorFilter filter) public SKShader WithLocalMatrix (SKMatrix localMatrix) { var shader = GetObject (SkiaApi.sk_shader_with_local_matrix (Handle, &localMatrix)); + GC.KeepAlive (this); return shader; } diff --git a/binding/SkiaSharp/SKStream.cs b/binding/SkiaSharp/SKStream.cs index 6fedca71833..3529b0f683a 100644 --- a/binding/SkiaSharp/SKStream.cs +++ b/binding/SkiaSharp/SKStream.cs @@ -15,6 +15,7 @@ internal SKStream (IntPtr handle, bool owns) public bool IsAtEnd { get { var result = SkiaApi.sk_stream_is_at_end (Handle); + GC.KeepAlive (this); return result; } } @@ -72,6 +73,7 @@ public bool ReadSByte (out SByte buffer) { fixed (SByte* b = &buffer) { var result = SkiaApi.sk_stream_read_s8 (Handle, b); + GC.KeepAlive (this); return result; } } @@ -80,6 +82,7 @@ public bool ReadInt16 (out Int16 buffer) { fixed (Int16* b = &buffer) { var result = SkiaApi.sk_stream_read_s16 (Handle, b); + GC.KeepAlive (this); return result; } } @@ -88,6 +91,7 @@ public bool ReadInt32 (out Int32 buffer) { fixed (Int32* b = &buffer) { var result = SkiaApi.sk_stream_read_s32 (Handle, b); + GC.KeepAlive (this); return result; } } @@ -96,6 +100,7 @@ public bool ReadByte (out Byte buffer) { fixed (Byte* b = &buffer) { var result = SkiaApi.sk_stream_read_u8 (Handle, b); + GC.KeepAlive (this); return result; } } @@ -104,6 +109,7 @@ public bool ReadUInt16 (out UInt16 buffer) { fixed (UInt16* b = &buffer) { var result = SkiaApi.sk_stream_read_u16 (Handle, b); + GC.KeepAlive (this); return result; } } @@ -112,6 +118,7 @@ public bool ReadUInt32 (out UInt32 buffer) { fixed (UInt32* b = &buffer) { var result = SkiaApi.sk_stream_read_u32 (Handle, b); + GC.KeepAlive (this); return result; } } @@ -120,6 +127,7 @@ public bool ReadBool (out Boolean buffer) { byte b; var result = SkiaApi.sk_stream_read_bool (Handle, &b); + GC.KeepAlive (this); buffer = b > 0; return result; } @@ -134,30 +142,35 @@ public int Read (byte[] buffer, int size) public int Read (IntPtr buffer, int size) { var result = (int)SkiaApi.sk_stream_read (Handle, (void*)buffer, (IntPtr)size); + GC.KeepAlive (this); return result; } public int Peek (IntPtr buffer, int size) { var result = (int)SkiaApi.sk_stream_peek (Handle, (void*)buffer, (IntPtr)size); + GC.KeepAlive (this); return result; } public int Skip (int size) { var result = (int)SkiaApi.sk_stream_skip (Handle, (IntPtr)size); + GC.KeepAlive (this); return result; } public bool Rewind () { var result = SkiaApi.sk_stream_rewind (Handle); + GC.KeepAlive (this); return result; } public bool Seek (int position) { var result = SkiaApi.sk_stream_seek (Handle, (IntPtr)position); + GC.KeepAlive (this); return result; } @@ -167,36 +180,42 @@ public bool Seek (int position) public bool Move (int offset) { var result = SkiaApi.sk_stream_move (Handle, offset); + GC.KeepAlive (this); return result; } public IntPtr GetMemoryBase () { var result = (IntPtr)SkiaApi.sk_stream_get_memory_base (Handle); + GC.KeepAlive (this); return result; } public SKData GetData () { var result = SKData.GetObject (SkiaApi.sk_stream_get_data (Handle)); + GC.KeepAlive (this); return result; } internal SKStream Fork () { var result = GetObject (SkiaApi.sk_stream_fork (Handle)); + GC.KeepAlive (this); return result; } internal SKStream Duplicate () { var result = GetObject (SkiaApi.sk_stream_duplicate (Handle)); + GC.KeepAlive (this); return result; } public bool HasPosition { get { var result = SkiaApi.sk_stream_has_position (Handle); + GC.KeepAlive (this); return result; } } @@ -204,6 +223,7 @@ public bool HasPosition { public int Position { get { var result = (int)SkiaApi.sk_stream_get_position (Handle); + GC.KeepAlive (this); return result; } set { @@ -214,6 +234,7 @@ public int Position { public bool HasLength { get { var result = SkiaApi.sk_stream_has_length (Handle); + GC.KeepAlive (this); return result; } } @@ -221,6 +242,7 @@ public bool HasLength { public int Length { get { var result = (int)SkiaApi.sk_stream_get_length (Handle); + GC.KeepAlive (this); return result; } } @@ -324,6 +346,7 @@ protected override void DisposeNative () => public bool IsValid { get { var result = SkiaApi.sk_filestream_is_valid (Handle); + GC.KeepAlive (this); return result; } } @@ -395,12 +418,14 @@ protected override void DisposeNative () => internal void SetMemory (IntPtr data, IntPtr length, bool copyData = false) { SkiaApi.sk_memorystream_set_memory (Handle, (void*)data, length, copyData); + GC.KeepAlive (this); } internal void SetMemory (byte[] data, IntPtr length, bool copyData = false) { fixed (byte* d = data) { SkiaApi.sk_memorystream_set_memory (Handle, d, length, copyData); + GC.KeepAlive (this); } } @@ -420,6 +445,7 @@ internal SKWStream (IntPtr handle, bool owns) public virtual int BytesWritten { get { var result = (int)SkiaApi.sk_wstream_bytes_written (Handle); + GC.KeepAlive (this); return result; } } @@ -428,6 +454,7 @@ public virtual bool Write (byte[] buffer, int size) { fixed (byte* b = buffer) { var result = SkiaApi.sk_wstream_write (Handle, (void*)b, (IntPtr)size); + GC.KeepAlive (this); return result; } } @@ -435,77 +462,90 @@ public virtual bool Write (byte[] buffer, int size) public bool NewLine () { var result = SkiaApi.sk_wstream_newline (Handle); + GC.KeepAlive (this); return result; } public virtual void Flush () { SkiaApi.sk_wstream_flush (Handle); + GC.KeepAlive (this); } public bool Write8 (Byte value) { var result = SkiaApi.sk_wstream_write_8 (Handle, value); + GC.KeepAlive (this); return result; } public bool Write16 (UInt16 value) { var result = SkiaApi.sk_wstream_write_16 (Handle, value); + GC.KeepAlive (this); return result; } public bool Write32 (UInt32 value) { var result = SkiaApi.sk_wstream_write_32 (Handle, value); + GC.KeepAlive (this); return result; } public bool WriteText (string value) { var result = SkiaApi.sk_wstream_write_text (Handle, value); + GC.KeepAlive (this); return result; } public bool WriteDecimalAsTest (Int32 value) { var result = SkiaApi.sk_wstream_write_dec_as_text (Handle, value); + GC.KeepAlive (this); return result; } public bool WriteBigDecimalAsText (Int64 value, int digits) { var result = SkiaApi.sk_wstream_write_bigdec_as_text (Handle, value, digits); + GC.KeepAlive (this); return result; } public bool WriteHexAsText (UInt32 value, int digits) { var result = SkiaApi.sk_wstream_write_hex_as_text (Handle, value, digits); + GC.KeepAlive (this); return result; } public bool WriteScalarAsText (float value) { var result = SkiaApi.sk_wstream_write_scalar_as_text (Handle, value); + GC.KeepAlive (this); return result; } public bool WriteBool (bool value) { var result = SkiaApi.sk_wstream_write_bool (Handle, value); + GC.KeepAlive (this); return result; } public bool WriteScalar (float value) { var result = SkiaApi.sk_wstream_write_scalar (Handle, value); + GC.KeepAlive (this); return result; } public bool WritePackedUInt32 (UInt32 value) { var result = SkiaApi.sk_wstream_write_packed_uint (Handle, (IntPtr)value); + GC.KeepAlive (this); return result; } @@ -517,6 +557,7 @@ public bool WriteStream (SKStream input, int length) var result = SkiaApi.sk_wstream_write_stream (Handle, input.Handle, (IntPtr)length); GC.KeepAlive (input); + GC.KeepAlive (this); return result; } @@ -558,6 +599,7 @@ protected override void DisposeNative () => public bool IsValid { get { var result = SkiaApi.sk_filewstream_is_valid (Handle); + GC.KeepAlive (this); return result; } } @@ -600,18 +642,21 @@ public SKData CopyToData () public SKStreamAsset DetachAsStream () { var result = SKStreamAssetImplementation.GetObject (SkiaApi.sk_dynamicmemorywstream_detach_as_stream (Handle)); + GC.KeepAlive (this); return result; } public SKData DetachAsData () { var result = SKData.GetObject (SkiaApi.sk_dynamicmemorywstream_detach_as_data (Handle)); + GC.KeepAlive (this); return result; } public void CopyTo (IntPtr data) { SkiaApi.sk_dynamicmemorywstream_copy_to (Handle, (void*)data); + GC.KeepAlive (this); } public void CopyTo (Span data) @@ -622,6 +667,7 @@ public void CopyTo (Span data) fixed (void* d = data) { SkiaApi.sk_dynamicmemorywstream_copy_to (Handle, d); + GC.KeepAlive (this); } } @@ -631,6 +677,7 @@ public bool CopyTo (SKWStream dst) throw new ArgumentNullException (nameof (dst)); var result = SkiaApi.sk_dynamicmemorywstream_write_to_stream (Handle, dst.Handle); GC.KeepAlive (dst); + GC.KeepAlive (this); return result; } diff --git a/binding/SkiaSharp/SKString.cs b/binding/SkiaSharp/SKString.cs index 79d30e025eb..5b080c851a5 100644 --- a/binding/SkiaSharp/SKString.cs +++ b/binding/SkiaSharp/SKString.cs @@ -50,6 +50,7 @@ public override string ToString () var cstr = SkiaApi.sk_string_get_c_str (Handle); var clen = SkiaApi.sk_string_get_size (Handle); var result = StringUtilities.GetString ((IntPtr)cstr, (int)clen, SKTextEncoding.Utf8); + GC.KeepAlive (this); return result; } diff --git a/binding/SkiaSharp/SKSurface.cs b/binding/SkiaSharp/SKSurface.cs index 96945c6a162..d8d8414e872 100644 --- a/binding/SkiaSharp/SKSurface.cs +++ b/binding/SkiaSharp/SKSurface.cs @@ -291,6 +291,7 @@ public static SKSurface CreateNull (int width, int height) => public SKCanvas Canvas { get { var result = OwnedBy (SKCanvas.GetObject (SkiaApi.sk_surface_get_canvas (Handle), false, unrefExisting: false), this); + GC.KeepAlive (this); return result; } } @@ -298,6 +299,7 @@ public SKCanvas Canvas { public SKSurfaceProperties SurfaceProperties { get { var result = OwnedBy (SKSurfaceProperties.GetObject (SkiaApi.sk_surface_get_props (Handle), false), this); + GC.KeepAlive (this); return result; } } @@ -305,6 +307,7 @@ public SKSurfaceProperties SurfaceProperties { public GRRecordingContext Context { get { var result = GRRecordingContext.GetObject (SkiaApi.sk_surface_get_recording_context (Handle), false, unrefExisting: false); + GC.KeepAlive (this); return result; } } @@ -312,12 +315,14 @@ public GRRecordingContext Context { public SKImage Snapshot () { var result = SKImage.GetObject (SkiaApi.sk_surface_new_image_snapshot (Handle)); + GC.KeepAlive (this); return result; } public SKImage Snapshot (SKRectI bounds) { var result = SKImage.GetObject (SkiaApi.sk_surface_new_image_snapshot_with_crop (Handle, &bounds)); + GC.KeepAlive (this); return result; } @@ -327,6 +332,7 @@ public void Draw (SKCanvas canvas, float x, float y, SKPaint paint) throw new ArgumentNullException (nameof (canvas)); SkiaApi.sk_surface_draw (Handle, canvas.Handle, x, y, paint == null ? IntPtr.Zero : paint.Handle); + GC.KeepAlive (this); GC.KeepAlive (canvas); GC.KeepAlive (paint); } @@ -342,6 +348,7 @@ public void Draw (SKCanvas canvas, float x, float y, SKSamplingOptions sampling, throw new ArgumentNullException (nameof (canvas)); SkiaApi.sk_surface_draw_with_sampling (Handle, canvas.Handle, x, y, &sampling, paint == null ? IntPtr.Zero : paint.Handle); + GC.KeepAlive (this); GC.KeepAlive (canvas); GC.KeepAlive (paint); } @@ -364,6 +371,7 @@ public bool PeekPixels (SKPixmap pixmap) throw new ArgumentNullException (nameof (pixmap)); var result = SkiaApi.sk_surface_peek_pixels (Handle, pixmap.Handle); + GC.KeepAlive (this); GC.KeepAlive (pixmap); if (result) pixmap.pixelSource = this; @@ -374,6 +382,7 @@ public bool ReadPixels (SKImageInfo dstInfo, IntPtr dstPixels, int dstRowBytes, { var cinfo = SKImageInfoNative.FromManaged (ref dstInfo); var result = SkiaApi.sk_surface_read_pixels (Handle, &cinfo, (void*)dstPixels, (IntPtr)dstRowBytes, srcX, srcY); + GC.KeepAlive (this); return result; } diff --git a/binding/SkiaSharp/SKTextBlob.cs b/binding/SkiaSharp/SKTextBlob.cs index a9016c1cea2..97dfffe48c3 100644 --- a/binding/SkiaSharp/SKTextBlob.cs +++ b/binding/SkiaSharp/SKTextBlob.cs @@ -15,17 +15,20 @@ protected override void Dispose (bool disposing) => void ISKNonVirtualReferenceCounted.ReferenceNative () { SkiaApi.sk_textblob_ref (Handle); + GC.KeepAlive (this); } void ISKNonVirtualReferenceCounted.UnreferenceNative () { SkiaApi.sk_textblob_unref (Handle); + GC.KeepAlive (this); } public SKRect Bounds { get { SKRect bounds; SkiaApi.sk_textblob_get_bounds (Handle, &bounds); + GC.KeepAlive (this); return bounds; } } @@ -33,6 +36,7 @@ public SKRect Bounds { public uint UniqueId { get { var r = SkiaApi.sk_textblob_get_unique_id (Handle); + GC.KeepAlive (this); return r; } } @@ -252,6 +256,7 @@ public void GetIntercepts (float upperBounds, float lowerBounds, Span int fixed (float* i = intervals) { SkiaApi.sk_textblob_get_intercepts (Handle, bounds, i, paint?.Handle ?? IntPtr.Zero); GC.KeepAlive (paint); + GC.KeepAlive (this); } } @@ -264,6 +269,7 @@ public int CountIntercepts (float upperBounds, float lowerBounds, SKPaint? paint bounds[1] = lowerBounds; var result = SkiaApi.sk_textblob_get_intercepts (Handle, bounds, null, paint?.Handle ?? IntPtr.Zero); GC.KeepAlive (paint); + GC.KeepAlive (this); return result; } @@ -291,6 +297,7 @@ protected override void Dispose (bool disposing) => protected override void DisposeNative () { SkiaApi.sk_textblob_builder_delete (Handle); + GC.KeepAlive (this); } // Build @@ -298,6 +305,7 @@ protected override void DisposeNative () public SKTextBlob? Build () { var blob = SKTextBlob.GetObject (SkiaApi.sk_textblob_builder_make (Handle)); + GC.KeepAlive (this); return blob; } @@ -412,6 +420,7 @@ public SKRawRunBuffer AllocateRawRun (SKFont font, int count, float x, fl SkiaApi.sk_textblob_builder_alloc_run (Handle, font.Handle, count, x, y, null, &runbuffer); GC.KeepAlive (font); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, 0, 0); } @@ -433,6 +442,7 @@ public SKRawRunBuffer AllocateRawTextRun (SKFont font, int count, float x SkiaApi.sk_textblob_builder_alloc_run_text (Handle, font.Handle, count, x, y, textByteCount, null, &runbuffer); GC.KeepAlive (font); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, 0, textByteCount); } @@ -456,6 +466,7 @@ public SKRawRunBuffer AllocateRawHorizontalRun (SKFont font, int count, f SkiaApi.sk_textblob_builder_alloc_run_pos_h (Handle, font.Handle, count, y, null, &runbuffer); GC.KeepAlive (font); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, count, 0); } @@ -477,6 +488,7 @@ public SKRawRunBuffer AllocateRawHorizontalTextRun (SKFont font, int coun SkiaApi.sk_textblob_builder_alloc_run_text_pos_h (Handle, font.Handle, count, y, textByteCount, null, &runbuffer); GC.KeepAlive (font); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, count, textByteCount); } @@ -501,6 +513,7 @@ public SKRawRunBuffer AllocateRawPositionedRun (SKFont font, int count, SkiaApi.sk_textblob_builder_alloc_run_pos (Handle, font.Handle, count, null, &runbuffer); GC.KeepAlive (font); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, count, 0); } @@ -522,6 +535,7 @@ public SKRawRunBuffer AllocateRawPositionedTextRun (SKFont font, int co SkiaApi.sk_textblob_builder_alloc_run_text_pos (Handle, font.Handle, count, textByteCount, null, &runbuffer); GC.KeepAlive (font); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, count, textByteCount); } @@ -545,6 +559,7 @@ public SKRawRunBuffer AllocateRawRotationScaleRun (SKFont SkiaApi.sk_textblob_builder_alloc_run_rsxform (Handle, font.Handle, count, null, &runbuffer); GC.KeepAlive (font); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, count, 0); } @@ -566,6 +581,7 @@ public SKRawRunBuffer AllocateRawRotationScaleTextRun (SK SkiaApi.sk_textblob_builder_alloc_run_text_rsxform (Handle, font.Handle, count, textByteCount, null, &runbuffer); GC.KeepAlive (font); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, count, textByteCount); } } diff --git a/binding/SkiaSharp/SKTypeface.cs b/binding/SkiaSharp/SKTypeface.cs index 3b7c15c4f24..012cca3dee6 100644 --- a/binding/SkiaSharp/SKTypeface.cs +++ b/binding/SkiaSharp/SKTypeface.cs @@ -97,6 +97,7 @@ public static SKTypeface FromData (SKData data, int index = 0) => public string FamilyName { get { var r = (string)SKString.GetObject (SkiaApi.sk_typeface_get_family_name (Handle)); + GC.KeepAlive (this); return r; } } @@ -104,6 +105,7 @@ public string FamilyName { public SKFontStyle FontStyle { get { var r = SKFontStyle.GetObject (SkiaApi.sk_typeface_get_fontstyle (Handle)); + GC.KeepAlive (this); return r; } } @@ -111,6 +113,7 @@ public SKFontStyle FontStyle { public int FontWeight { get { var r = SkiaApi.sk_typeface_get_font_weight (Handle); + GC.KeepAlive (this); return r; } } @@ -118,6 +121,7 @@ public int FontWeight { public int FontWidth { get { var r = SkiaApi.sk_typeface_get_font_width (Handle); + GC.KeepAlive (this); return r; } } @@ -125,6 +129,7 @@ public int FontWidth { public SKFontStyleSlant FontSlant { get { var r = SkiaApi.sk_typeface_get_font_slant (Handle); + GC.KeepAlive (this); return r; } } @@ -136,6 +141,7 @@ public SKFontStyleSlant FontSlant { public bool IsFixedPitch { get { var r = SkiaApi.sk_typeface_is_fixed_pitch (Handle); + GC.KeepAlive (this); return r; } } @@ -143,6 +149,7 @@ public bool IsFixedPitch { public int UnitsPerEm { get { var r = SkiaApi.sk_typeface_get_units_per_em (Handle); + GC.KeepAlive (this); return r; } } @@ -150,6 +157,7 @@ public int UnitsPerEm { public int GlyphCount { get { var r = SkiaApi.sk_typeface_count_glyphs (Handle); + GC.KeepAlive (this); return r; } } @@ -157,6 +165,7 @@ public int GlyphCount { public string PostScriptName { get { var r = (string)SKString.GetObject (SkiaApi.sk_typeface_get_post_script_name (Handle)); + GC.KeepAlive (this); return r; } } @@ -166,6 +175,7 @@ public string PostScriptName { public int TableCount { get { var r = SkiaApi.sk_typeface_count_tables (Handle); + GC.KeepAlive (this); return r; } } @@ -183,9 +193,11 @@ public bool TryGetTableTags (out UInt32[] tags) var buffer = new UInt32[TableCount]; fixed (UInt32* b = buffer) { if (SkiaApi.sk_typeface_get_table_tags (Handle, b) == 0) { + GC.KeepAlive (this); tags = null; return false; } + GC.KeepAlive (this); } tags = buffer; return true; @@ -196,6 +208,7 @@ public bool TryGetTableTags (out UInt32[] tags) public int GetTableSize (UInt32 tag) { var r = (int)SkiaApi.sk_typeface_get_table_size (Handle, tag); + GC.KeepAlive (this); return r; } @@ -226,6 +239,7 @@ public bool TryGetTableData (UInt32 tag, out byte[] tableData) public bool TryGetTableData (UInt32 tag, int offset, int length, IntPtr tableData) { var actual = SkiaApi.sk_typeface_get_table_data (Handle, tag, (IntPtr)offset, (IntPtr)length, (byte*)tableData); + GC.KeepAlive (this); return actual != IntPtr.Zero; } @@ -321,6 +335,7 @@ public SKStreamAsset OpenStream (out int ttcIndex) { fixed (int* ttc = &ttcIndex) { var r = SKStreamAsset.GetObject (SkiaApi.sk_typeface_open_stream (Handle, ttc)); + GC.KeepAlive (this); return r; } } @@ -334,6 +349,7 @@ public SKStreamAsset OpenStream (out int ttcIndex) public bool HasGetKerningPairAdjustments { get { var r = SkiaApi.sk_typeface_get_kerning_pair_adjustments (Handle, null, 0, null); + GC.KeepAlive (this); return r; } } @@ -381,6 +397,7 @@ public bool GetKerningPairAdjustments (ReadOnlySpan glyphs, Span ad fixed (ushort* gp = glyphs) fixed (int* ap = adjustments) { res = SkiaApi.sk_typeface_get_kerning_pair_adjustments (Handle, gp, glyphs.Length, ap); + GC.KeepAlive (this); } if (!res && glyphs.Length > 1) @@ -397,6 +414,7 @@ public bool GetKerningPairAdjustments (ReadOnlySpan glyphs, Span ad public int VariationDesignParameterCount { get { var r = SkiaApi.sk_typeface_get_variation_design_parameters (Handle, null, 0); + GC.KeepAlive (this); return r; } } @@ -411,6 +429,7 @@ public SKFontVariationAxis[] VariationDesignParameters var axes = new SKFontVariationAxis[count]; fixed (SKFontVariationAxis* ptr = axes) { SkiaApi.sk_typeface_get_variation_design_parameters (Handle, ptr, count); + GC.KeepAlive (this); } return axes; } @@ -424,6 +443,7 @@ public int GetVariationDesignParameters (Span axes) fixed (SKFontVariationAxis* ptr = axes) { var total = SkiaApi.sk_typeface_get_variation_design_parameters (Handle, ptr, axes.Length); if (total <= axes.Length) { + GC.KeepAlive (this); return total; } @@ -432,6 +452,7 @@ public int GetVariationDesignParameters (Span axes) using var temp = Utils.RentArray (total); fixed (SKFontVariationAxis* tempPtr = temp.Span) { SkiaApi.sk_typeface_get_variation_design_parameters (Handle, tempPtr, total); + GC.KeepAlive (this); } temp.Span.Slice (0, axes.Length).CopyTo (axes); return axes.Length; @@ -441,6 +462,7 @@ public int GetVariationDesignParameters (Span axes) public int VariationDesignPositionCount { get { var r = SkiaApi.sk_typeface_get_variation_design_position (Handle, null, 0); + GC.KeepAlive (this); return r; } } @@ -455,6 +477,7 @@ public SKFontVariationPositionCoordinate[] VariationDesignPosition var coords = new SKFontVariationPositionCoordinate[count]; fixed (SKFontVariationPositionCoordinate* ptr = coords) { SkiaApi.sk_typeface_get_variation_design_position (Handle, ptr, count); + GC.KeepAlive (this); } return coords; } @@ -468,6 +491,7 @@ public int GetVariationDesignPosition (Span c fixed (SKFontVariationPositionCoordinate* ptr = coordinates) { var total = SkiaApi.sk_typeface_get_variation_design_position (Handle, ptr, coordinates.Length); if (total <= coordinates.Length) { + GC.KeepAlive (this); return total; } @@ -476,6 +500,7 @@ public int GetVariationDesignPosition (Span c using var temp = Utils.RentArray (total); fixed (SKFontVariationPositionCoordinate* tempPtr = temp.Span) { SkiaApi.sk_typeface_get_variation_design_position (Handle, tempPtr, total); + GC.KeepAlive (this); } temp.Span.Slice (0, coordinates.Length).CopyTo (coordinates); return coordinates.Length; @@ -486,6 +511,7 @@ public SKTypeface Clone (ReadOnlySpan positio { fixed (SKFontVariationPositionCoordinate* ptr = position) { var r = GetObject (SkiaApi.sk_typeface_clone_with_arguments (Handle, ptr, position.Length, 0, 0, null, 0)); + GC.KeepAlive (this); return r; } } @@ -495,6 +521,7 @@ public SKTypeface Clone (int paletteIndex) if (paletteIndex < 0) throw new ArgumentOutOfRangeException (nameof (paletteIndex)); var r = GetObject (SkiaApi.sk_typeface_clone_with_arguments (Handle, null, 0, 0, paletteIndex, null, 0)); + GC.KeepAlive (this); return r; } @@ -503,6 +530,7 @@ public SKTypeface Clone (SKFontArguments args) fixed (SKFontVariationPositionCoordinate* posPtr = args.VariationDesignPosition) fixed (SKFontPaletteOverride* palPtr = args.PaletteOverrides) { var r = GetObject (SkiaApi.sk_typeface_clone_with_arguments (Handle, posPtr, args.VariationDesignPosition.Length, args.CollectionIndex, args.PaletteIndex, palPtr, args.PaletteOverrides.Length)); + GC.KeepAlive (this); return r; } } From a260adb368c1d84da640c7c48658dbbfcbeb769a Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Tue, 9 Jun 2026 11:23:09 -0400 Subject: [PATCH 27/29] Extend lifetime KeepAlives to Skottie/SceneGraph/Resources bindings The 1C pass missed the satellite binding assemblies (they use SkottieApi/SceneGraphApi/ResourcesApi, not SkiaApi). SkiaSharp.Skottie.Animation.Version was a confirmed crash: skottie_animation_get_version(Handle, ...) with 'this' collectible mid-call. Add GC.KeepAlive(this) (and wrapper-arg KeepAlives) to their instance native calls. Co-Authored-By: Claude Opus 4.8 --- .../SkiaSharp.Resources/ResourceProvider.cs | 8 ++- .../InvalidationController.cs | 5 ++ binding/SkiaSharp.Skottie/Animation.cs | 68 +++++++++++++++---- binding/SkiaSharp.Skottie/AnimationBuilder.cs | 6 ++ 4 files changed, 72 insertions(+), 15 deletions(-) diff --git a/binding/SkiaSharp.Resources/ResourceProvider.cs b/binding/SkiaSharp.Resources/ResourceProvider.cs index 5b22bc5c7d1..a70e714b7b2 100644 --- a/binding/SkiaSharp.Resources/ResourceProvider.cs +++ b/binding/SkiaSharp.Resources/ResourceProvider.cs @@ -12,8 +12,12 @@ internal ResourceProvider (IntPtr handle, bool owns) public SKData? Load (string resourceName) => Load ("", resourceName); - public SKData? Load (string resourcePath, string resourceName) => - SKData.GetObject (ResourcesApi.skresources_resource_provider_load (Handle, resourcePath, resourceName)); + public SKData? Load (string resourcePath, string resourceName) + { + var r = SKData.GetObject (ResourcesApi.skresources_resource_provider_load (Handle, resourcePath, resourceName)); + GC.KeepAlive (this); + return r; + } } public sealed class CachingResourceProvider : ResourceProvider diff --git a/binding/SkiaSharp.SceneGraph/InvalidationController.cs b/binding/SkiaSharp.SceneGraph/InvalidationController.cs index bb270059796..f6b6f97058e 100644 --- a/binding/SkiaSharp.SceneGraph/InvalidationController.cs +++ b/binding/SkiaSharp.SceneGraph/InvalidationController.cs @@ -27,12 +27,14 @@ protected override void DisposeNative () public unsafe void Invalidate (SKRect rect, SKMatrix matrix) { SceneGraphApi.sksg_invalidation_controller_inval (Handle, &rect, &matrix); + GC.KeepAlive (this); } public unsafe SKRect Bounds { get { SKRect rect; SceneGraphApi.sksg_invalidation_controller_get_bounds (Handle, &rect); + GC.KeepAlive (this); return rect; } } @@ -40,16 +42,19 @@ public unsafe SKRect Bounds { public unsafe void Begin () { SceneGraphApi.sksg_invalidation_controller_begin (Handle); + GC.KeepAlive (this); } public unsafe void End () { SceneGraphApi.sksg_invalidation_controller_end (Handle); + GC.KeepAlive (this); } public unsafe void Reset () { SceneGraphApi.sksg_invalidation_controller_reset (Handle); + GC.KeepAlive (this); } } } diff --git a/binding/SkiaSharp.Skottie/Animation.cs b/binding/SkiaSharp.Skottie/Animation.cs index a7360d73285..8b71e172542 100644 --- a/binding/SkiaSharp.Skottie/Animation.cs +++ b/binding/SkiaSharp.Skottie/Animation.cs @@ -104,43 +104,84 @@ public static bool TryCreate (string path, [System.Diagnostics.CodeAnalysis.NotN // Render public unsafe void Render (SKCanvas canvas, SKRect dst) - => SkottieApi.skottie_animation_render (Handle, canvas.Handle, &dst); + { + SkottieApi.skottie_animation_render (Handle, canvas.Handle, &dst); + GC.KeepAlive (this); + GC.KeepAlive (canvas); + } public void Render (SKCanvas canvas, SKRect dst, AnimationRenderFlags flags) - => SkottieApi.skottie_animation_render_with_flags (Handle, canvas.Handle, &dst, flags); + { + SkottieApi.skottie_animation_render_with_flags (Handle, canvas.Handle, &dst, flags); + GC.KeepAlive (this); + GC.KeepAlive (canvas); + } // Seek* public void Seek (double percent, InvalidationController? ic = null) - => SkottieApi.skottie_animation_seek (Handle, (float)percent, ic?.Handle ?? IntPtr.Zero); + { + SkottieApi.skottie_animation_seek (Handle, (float)percent, ic?.Handle ?? IntPtr.Zero); + GC.KeepAlive (this); + GC.KeepAlive (ic); + } public void SeekFrame (double frame, InvalidationController? ic = null) - => SkottieApi.skottie_animation_seek_frame (Handle, (float)frame, ic?.Handle ?? IntPtr.Zero); + { + SkottieApi.skottie_animation_seek_frame (Handle, (float)frame, ic?.Handle ?? IntPtr.Zero); + GC.KeepAlive (this); + GC.KeepAlive (ic); + } public void SeekFrameTime (double seconds, InvalidationController? ic = null) - => SkottieApi.skottie_animation_seek_frame_time (Handle, (float)seconds, ic?.Handle ?? IntPtr.Zero); + { + SkottieApi.skottie_animation_seek_frame_time (Handle, (float)seconds, ic?.Handle ?? IntPtr.Zero); + GC.KeepAlive (this); + GC.KeepAlive (ic); + } public void SeekFrameTime (TimeSpan time, InvalidationController? ic = null) => SeekFrameTime (time.TotalSeconds, ic); // Properties - public TimeSpan Duration - => TimeSpan.FromSeconds (SkottieApi.skottie_animation_get_duration (Handle)); + public TimeSpan Duration { + get { + var r = SkottieApi.skottie_animation_get_duration (Handle); + GC.KeepAlive (this); + return TimeSpan.FromSeconds (r); + } + } - public double Fps - => SkottieApi.skottie_animation_get_fps (Handle); + public double Fps { + get { + var r = SkottieApi.skottie_animation_get_fps (Handle); + GC.KeepAlive (this); + return r; + } + } - public double InPoint - => SkottieApi.skottie_animation_get_in_point (Handle); + public double InPoint { + get { + var r = SkottieApi.skottie_animation_get_in_point (Handle); + GC.KeepAlive (this); + return r; + } + } - public double OutPoint - => SkottieApi.skottie_animation_get_out_point (Handle); + public double OutPoint { + get { + var r = SkottieApi.skottie_animation_get_out_point (Handle); + GC.KeepAlive (this); + return r; + } + } public string Version { get { using var str = new SKString (); SkottieApi.skottie_animation_get_version (Handle, str.Handle); + GC.KeepAlive (this); return str.ToString (); } } @@ -149,6 +190,7 @@ public unsafe SKSize Size { get { SKSize size; SkottieApi.skottie_animation_get_size (Handle, &size); + GC.KeepAlive (this); return size; } } diff --git a/binding/SkiaSharp.Skottie/AnimationBuilder.cs b/binding/SkiaSharp.Skottie/AnimationBuilder.cs index b84f563a36a..caf585ab78a 100644 --- a/binding/SkiaSharp.Skottie/AnimationBuilder.cs +++ b/binding/SkiaSharp.Skottie/AnimationBuilder.cs @@ -20,6 +20,8 @@ public AnimationBuilder SetFontManager (SKFontManager fontManager) { _ = fontManager ?? throw new ArgumentNullException (nameof (fontManager)); SkottieApi.skottie_animation_builder_set_font_manager (Handle, fontManager.Handle); + GC.KeepAlive (this); + GC.KeepAlive (fontManager); Referenced (this, fontManager); return this; } @@ -28,6 +30,8 @@ public AnimationBuilder SetResourceProvider (ResourceProvider resourceProvider) { _ = resourceProvider ?? throw new ArgumentNullException (nameof (resourceProvider)); SkottieApi.skottie_animation_builder_set_resource_provider (Handle, resourceProvider.Handle); + GC.KeepAlive (this); + GC.KeepAlive (resourceProvider); Referenced (this, resourceProvider); return this; } @@ -38,6 +42,7 @@ public AnimationBuilderStats Stats { AnimationBuilderStats stats; SkottieApi.skottie_animation_builder_get_stats (Handle, &stats); + GC.KeepAlive (this); return stats; } } @@ -69,6 +74,7 @@ public AnimationBuilderStats Stats try { return Animation.GetObject (SkottieApi.skottie_animation_builder_make_from_data (Handle, ptr, (IntPtr)span.Length)); } finally { + GC.KeepAlive (this); GC.KeepAlive(data); } } From 14f9a29f7d680f5c8e2ae8246d28090a8439ba11 Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Tue, 9 Jun 2026 19:24:32 -0400 Subject: [PATCH 28/29] Fix teardown heap corruption: never unref immortal Skia singletons The process-lifetime singleton accessors wrapped immortal Skia singletons with owns:true, so the managed finalizer called SafeUnRef on them at teardown. Driving the native refcount of an immortal singleton to zero runs its C++ destructor, which calls free()/delete on non-heap static storage, producing STATUS_HEAP_CORRUPTION (0xC0000374) on the finalizer thread. Captured via cdb: ntdll!RtlFreeHeap -> _free_base -> SkColorSpaceXformColorFilter::~SkColorSpaceXformColorFilter, driven by SKColorFilter.Finalize -> DisposeNative -> SafeUnRef on the srgbToLinear static. The native handle resolved to libSkiaSharp!gSingleton (static data), not the heap. Affected accessors return SkNoDestructor / function-local-static singletons that Skia never destroys: - SKColorFilter.CreateSrgbToLinearGamma / CreateLinearToSrgbGamma - SKColorSpace.CreateSrgb / CreateSrgbLinear - SKData.Empty - SKBlender mode singletons - SKTypeface.Empty Wrap these with owns:false so Dispose(bool) skips DisposeNative (it gates on OwnsHandle). The binding leaks its single ref, which is correct: the object is immortal and never freed by Skia anyway. Mortal dispose-protected wrappers (font-manager match results, default font manager) keep owns:true. Full net48 suite: 8/8 runs crashed at teardown before; 0/14 after. Co-Authored-By: Claude Opus 4.8 --- binding/SkiaSharp/SKBlender.cs | 8 +++++--- binding/SkiaSharp/SKColorFilter.cs | 16 ++++++++++++---- binding/SkiaSharp/SKColorSpace.cs | 7 +++++-- binding/SkiaSharp/SKData.cs | 8 +++++--- binding/SkiaSharp/SKTypeface.cs | 8 +++++--- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/binding/SkiaSharp/SKBlender.cs b/binding/SkiaSharp/SKBlender.cs index a1549ef7891..5379110c202 100644 --- a/binding/SkiaSharp/SKBlender.cs +++ b/binding/SkiaSharp/SKBlender.cs @@ -44,7 +44,9 @@ static SKBlender () blendModeBlenders = new Dictionary (modes.Length); foreach (SKBlendMode mode in modes) { - blendModeBlenders[mode] = GetDisposeProtectedObject (SkiaApi.sk_blender_new_mode (mode)); + // Immortal Skia singletons (SkNoDestructor per mode) — never unref them. + // See SKColorFilter.GetDisposeProtectedObject for the full teardown-crash rationale. + blendModeBlenders[mode] = GetDisposeProtectedObject (SkiaApi.sk_blender_new_mode (mode), owns: false, unrefExisting: false); } } @@ -69,6 +71,6 @@ public static SKBlender CreateArithmetic (float k1, float k2, float k3, float k4 internal static SKBlender GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKBlender (h, o)); - internal static SKBlender GetDisposeProtectedObject (IntPtr handle) => - GetOrAddDisposeProtectedObject (handle, owns: true, unrefExisting: true, (h, o) => new SKBlender (h, o)); + internal static SKBlender GetDisposeProtectedObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => + GetOrAddDisposeProtectedObject (handle, owns, unrefExisting, (h, o) => new SKBlender (h, o)); } diff --git a/binding/SkiaSharp/SKColorFilter.cs b/binding/SkiaSharp/SKColorFilter.cs index 2b9e66ac205..1f9c9a66fd1 100644 --- a/binding/SkiaSharp/SKColorFilter.cs +++ b/binding/SkiaSharp/SKColorFilter.cs @@ -29,12 +29,12 @@ protected override void Dispose (bool disposing) => public static SKColorFilter CreateSrgbToLinearGamma () => LazyInitializer.EnsureInitialized ( ref srgbToLinear, ref srgbToLinearInitialized, ref srgbToLinearLock, - () => GetDisposeProtectedObject (SkiaApi.sk_colorfilter_new_srgb_to_linear_gamma ())); + () => GetDisposeProtectedObject (SkiaApi.sk_colorfilter_new_srgb_to_linear_gamma (), owns: false, unrefExisting: false)); public static SKColorFilter CreateLinearToSrgbGamma () => LazyInitializer.EnsureInitialized ( ref linearToSrgb, ref linearToSrgbInitialized, ref linearToSrgbLock, - () => GetDisposeProtectedObject (SkiaApi.sk_colorfilter_new_linear_to_srgb_gamma ())); + () => GetDisposeProtectedObject (SkiaApi.sk_colorfilter_new_linear_to_srgb_gamma (), owns: false, unrefExisting: false)); public static SKColorFilter CreateBlendMode(SKColor c, SKBlendMode mode) { @@ -160,7 +160,15 @@ public static SKColorFilter CreateHighContrast(bool grayscale, SKHighContrastCon internal static SKColorFilter GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKColorFilter (h, o)); - internal static SKColorFilter GetDisposeProtectedObject (IntPtr handle) => - GetOrAddDisposeProtectedObject (handle, owns: true, unrefExisting: true, (h, o) => new SKColorFilter (h, o)); + // owns/unrefExisting default to true for the general dispose-protected (but mortal) case. + // The srgb<->linear gamma singletons pass owns:false because Skia returns an *immortal* + // SkNoDestructor singleton (gSingleton) living in static storage. Unreffing it at + // finalization drives the native refcount to 0 and runs ~SkColorSpaceXformColorFilter, + // which calls free()/delete on non-heap memory => STATUS_HEAP_CORRUPTION at teardown. + // owns:false makes Dispose(bool) skip DisposeNative (it gates on OwnsHandle), so the + // binding never releases the immortal static. Leaking our single ref is correct: the + // object is never destroyed by Skia anyway. + internal static SKColorFilter GetDisposeProtectedObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => + GetOrAddDisposeProtectedObject (handle, owns, unrefExisting, (h, o) => new SKColorFilter (h, o)); } } diff --git a/binding/SkiaSharp/SKColorSpace.cs b/binding/SkiaSharp/SKColorSpace.cs index 8af2b8f1814..df9ad060812 100644 --- a/binding/SkiaSharp/SKColorSpace.cs +++ b/binding/SkiaSharp/SKColorSpace.cs @@ -83,14 +83,17 @@ public static bool Equal (SKColorSpace left, SKColorSpace right) public static SKColorSpace CreateSrgb () => LazyInitializer.EnsureInitialized ( ref srgb, ref srgbInitialized, ref srgbLock, - () => GetDisposeProtectedObject (SkiaApi.sk_colorspace_new_srgb ())); + // Immortal Skia singleton (sk_srgb_singleton, function-local static) — never unref it. + // See SKColorFilter.GetDisposeProtectedObject for the full teardown-crash rationale. + () => GetDisposeProtectedObject (SkiaApi.sk_colorspace_new_srgb (), owns: false, unrefExisting: false)); // CreateSrgbLinear public static SKColorSpace CreateSrgbLinear () => LazyInitializer.EnsureInitialized ( ref srgbLinear, ref srgbLinearInitialized, ref srgbLinearLock, - () => GetDisposeProtectedObject (SkiaApi.sk_colorspace_new_srgb_linear ())); + // Immortal Skia singleton (sk_srgb_linear_singleton, function-local static) — never unref it. + () => GetDisposeProtectedObject (SkiaApi.sk_colorspace_new_srgb_linear (), owns: false, unrefExisting: false)); // CreateIcc diff --git a/binding/SkiaSharp/SKData.cs b/binding/SkiaSharp/SKData.cs index 6ba80e76711..c0627b12184 100644 --- a/binding/SkiaSharp/SKData.cs +++ b/binding/SkiaSharp/SKData.cs @@ -43,7 +43,9 @@ void ISKNonVirtualReferenceCounted.UnreferenceNative () public static SKData Empty => LazyInitializer.EnsureInitialized ( ref empty, ref emptyInitialized, ref emptyLock, - () => GetDisposeProtectedObject (SkiaApi.sk_data_new_empty ())); + // Immortal Skia singleton (SkData::MakeEmpty's function-local static) — never unref it. + // See SKColorFilter.GetDisposeProtectedObject for the full teardown-crash rationale. + () => GetDisposeProtectedObject (SkiaApi.sk_data_new_empty (), owns: false, unrefExisting: false)); // CreateCopy @@ -300,8 +302,8 @@ public void SaveTo (Stream target) internal static SKData GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKData (h, o)); - internal static SKData GetDisposeProtectedObject (IntPtr handle) => - GetOrAddDisposeProtectedObject (handle, owns: true, unrefExisting: true, (h, o) => new SKData (h, o)); + internal static SKData GetDisposeProtectedObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => + GetOrAddDisposeProtectedObject (handle, owns, unrefExisting, (h, o) => new SKData (h, o)); // diff --git a/binding/SkiaSharp/SKTypeface.cs b/binding/SkiaSharp/SKTypeface.cs index 012cca3dee6..73aa2d9361d 100644 --- a/binding/SkiaSharp/SKTypeface.cs +++ b/binding/SkiaSharp/SKTypeface.cs @@ -45,7 +45,9 @@ protected override void Dispose (bool disposing) => public static SKTypeface Empty => LazyInitializer.EnsureInitialized ( ref empty, ref emptyInitialized, ref emptyLock, - () => GetDisposeProtectedObject (SkiaApi.sk_typeface_create_empty ())); + // Immortal Skia singleton (SkNoDestructor) — never unref it. + // See SKColorFilter.GetDisposeProtectedObject for the full teardown-crash rationale. + () => GetDisposeProtectedObject (SkiaApi.sk_typeface_create_empty (), owns: false, unrefExisting: false)); public bool IsEmpty => GlyphCount == 0; @@ -540,8 +542,8 @@ public SKTypeface Clone (SKFontArguments args) internal static SKTypeface GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKTypeface (h, o)); - internal static SKTypeface GetDisposeProtectedObject (IntPtr handle) => - GetOrAddDisposeProtectedObject (handle, owns: true, unrefExisting: true, (h, o) => new SKTypeface (h, o)); + internal static SKTypeface GetDisposeProtectedObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => + GetOrAddDisposeProtectedObject (handle, owns, unrefExisting, (h, o) => new SKTypeface (h, o)); } } From 52d8a46a1a2faba078e55ab82628593716e8bfd2 Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Tue, 9 Jun 2026 22:29:36 -0400 Subject: [PATCH 29/29] Skip PR #4080 use-after-free test on single-threaded WASM The test spins a GC-hammer thread, which throws System.PlatformNotSupportedException (Arg_PlatformNotSupported) on single-threaded WASM where Thread.Start is unsupported. Guard with SkipOnPlatform(IsBrowser, ...) like the sibling concurrency tests. Co-Authored-By: Claude Opus 4.8 --- tests/Tests/SkiaSharp/Pr4080UseAfterFreeTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Tests/SkiaSharp/Pr4080UseAfterFreeTest.cs b/tests/Tests/SkiaSharp/Pr4080UseAfterFreeTest.cs index 637171a5fe5..63c65cb551b 100644 --- a/tests/Tests/SkiaSharp/Pr4080UseAfterFreeTest.cs +++ b/tests/Tests/SkiaSharp/Pr4080UseAfterFreeTest.cs @@ -31,6 +31,8 @@ public class Pr4080UseAfterFreeTest : SKTest [SkippableFact] public void PathMeasureCtorKeepsPathAliveAcrossNativeCall () { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + var stop = 0; // Continuously collect and drain finalizers so any wrapper that becomes unrooted