diff --git a/binding/HarfBuzzSharp/Blob.cs b/binding/HarfBuzzSharp/Blob.cs index 12888988c05..9f662740325 100644 --- a/binding/HarfBuzzSharp/Blob.cs +++ b/binding/HarfBuzzSharp/Blob.cs @@ -36,18 +36,41 @@ 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); + GC.KeepAlive (this); + return r; + } + } - public int FaceCount => (int)HarfBuzzApi.hb_face_count (Handle); + public int FaceCount { + get { + var r = (int)HarfBuzzApi.hb_face_count (Handle); + GC.KeepAlive (this); + return r; + } + } - public bool IsImmutable => HarfBuzzApi.hb_blob_is_immutable (Handle); + public bool IsImmutable { + get { + var r = HarfBuzzApi.hb_blob_is_immutable (Handle); + GC.KeepAlive (this); + return r; + } + } - public void MakeImmutable () => HarfBuzzApi.hb_blob_make_immutable (Handle); + 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); } @@ -55,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 f34c24b1392..625296a8b8a 100644 --- a/binding/HarfBuzzSharp/Buffer.cs +++ b/binding/HarfBuzzSharp/Buffer.cs @@ -22,53 +22,125 @@ 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); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_content_type (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_direction (Handle, value); + GC.KeepAlive (this); + } } 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)); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_language (Handle, value.Handle); + GC.KeepAlive (value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_flags (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_cluster_level (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_replacement_codepoint (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_invisible_glyph (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_script (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_length (Handle, (uint)value); + GC.KeepAlive (this); + } } 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)); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_buffer_set_unicode_funcs (Handle, value.Handle); + GC.KeepAlive (value); + GC.KeepAlive (this); + } } public GlyphInfo[] GlyphInfos { @@ -97,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); @@ -124,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); @@ -164,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)); @@ -206,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); @@ -238,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); } @@ -251,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); } @@ -260,11 +339,20 @@ 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); + public void ClearContents () + { + HarfBuzzApi.hb_buffer_clear_contents (Handle); + GC.KeepAlive (this); + } - public void Reset () => HarfBuzzApi.hb_buffer_reset (Handle); + public void Reset () + { + HarfBuzzApi.hb_buffer_reset (Handle); + GC.KeepAlive (this); + } public void Append (Buffer buffer) => Append (buffer, 0, -1); @@ -276,6 +364,8 @@ 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); + GC.KeepAlive (this); } public void NormalizeGlyphs () @@ -286,14 +376,26 @@ 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); + public void Reverse () + { + HarfBuzzApi.hb_buffer_reverse (Handle); + GC.KeepAlive (this); + } - 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)); + GC.KeepAlive (this); + } - public void ReverseClusters () => HarfBuzzApi.hb_buffer_reverse_clusters (Handle); + public void ReverseClusters () + { + HarfBuzzApi.hb_buffer_reverse_clusters (Handle); + GC.KeepAlive (this); + } public string SerializeGlyphs () => SerializeGlyphs (0, -1, null, SerializeFormat.Text, SerializeFlag.Default); @@ -340,6 +442,9 @@ public unsafe string SerializeGlyphs (int start, int end, Font font, SerializeFo builder.Append (Marshal.PtrToStringAnsi ((IntPtr)pinned.Pointer, (int)consumed)); } + GC.KeepAlive (font); + GC.KeepAlive (this); + return builder.ToString (); } @@ -357,6 +462,8 @@ 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); + GC.KeepAlive (this); } protected override void Dispose (bool disposing) => diff --git a/binding/HarfBuzzSharp/Face.cs b/binding/HarfBuzzSharp/Face.cs index 1dc21ff2a6d..baaf6cb4847 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,39 @@ 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); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_face_set_index (Handle, (uint)value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_face_set_upem (Handle, (uint)value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + HarfBuzzApi.hb_face_set_glyph_count (Handle, (uint)value); + GC.KeepAlive (this); + } } public unsafe Tag[] Tables { @@ -74,35 +96,64 @@ public unsafe Tag[] Tables { fixed (void* ptr = buffer) { HarfBuzzApi.hb_face_get_table_tags (Handle, 0, &count, (uint*)ptr); } + GC.KeepAlive (this); return buffer; } } - 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)); + GC.KeepAlive (this); + return r; + } - public bool IsImmutable => HarfBuzzApi.hb_face_is_immutable (Handle); + public bool IsImmutable { + get { + var r = HarfBuzzApi.hb_face_is_immutable (Handle); + GC.KeepAlive (this); + return r; + } + } - public void MakeImmutable () => HarfBuzzApi.hb_face_make_immutable (Handle); + public void MakeImmutable () + { + HarfBuzzApi.hb_face_make_immutable (Handle); + GC.KeepAlive (this); + } // 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return r; + } + } public OpenTypeVarAxisInfo[] VariationAxisInfos { get { var count = HarfBuzzApi.hb_ot_var_get_axis_count (Handle); - if (count == 0) + if (count == 0) { + GC.KeepAlive (this); return Array.Empty (); + } var axes = new OpenTypeVarAxisInfo[(int)count]; fixed (OpenTypeVarAxisInfo* ptr = axes) { HarfBuzzApi.hb_ot_var_get_axis_infos (Handle, 0, &count, ptr); } + GC.KeepAlive (this); return axes; } } @@ -113,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; } @@ -120,25 +172,36 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return r; } public int GetNamedInstanceDesignCoordsCount (int instanceIndex) @@ -147,7 +210,9 @@ 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); + GC.KeepAlive (this); + return r; } public float[] GetNamedInstanceDesignCoords (int instanceIndex) @@ -157,14 +222,17 @@ 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) { + GC.KeepAlive (this); return Array.Empty (); + } uint coordsLength = (uint)totalCoords; var coords = new float[totalCoords]; fixed (float* ptr = coords) { HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, &coordsLength, ptr); } + GC.KeepAlive (this); return coords; } @@ -177,14 +245,27 @@ 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; } // 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return r; + } + } public HBColor[] GetPaletteColors (int paletteIndex) { @@ -192,14 +273,17 @@ 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) { + GC.KeepAlive (this); return Array.Empty (); + } uint count = (uint)totalColors; var colors = new HBColor[totalColors]; fixed (HBColor* ptr = colors) { HarfBuzzApi.hb_ot_color_palette_get_colors (Handle, (uint)paletteIndex, 0, &count, ptr); } + GC.KeepAlive (this); return colors; } @@ -212,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; } @@ -219,28 +304,52 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return r; + } + } protected override void Dispose (bool disposing) => diff --git a/binding/HarfBuzzSharp/Font.cs b/binding/HarfBuzzSharp/Font.cs index 835bcc15e2f..52915563a4d 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,8 @@ 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); + GC.KeepAlive (this); } public void GetScale (out int xScale, out int yScale) @@ -63,22 +66,30 @@ 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) => + 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) { - return HarfBuzzApi.hb_font_get_h_extents (Handle, e); + var r = HarfBuzzApi.hb_font_get_h_extents (Handle, e); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return r; } } @@ -88,7 +99,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -98,7 +111,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -108,15 +123,25 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return r; + } public unsafe int[] GetHorizontalGlyphAdvances (ReadOnlySpan glyphs) { @@ -133,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; } @@ -151,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; } @@ -158,7 +185,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -166,17 +195,25 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return r; } } @@ -184,7 +221,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -195,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; } @@ -209,7 +250,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -225,7 +268,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -233,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; } @@ -242,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) @@ -259,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; } @@ -266,7 +314,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -277,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 { @@ -287,7 +338,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -298,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) @@ -305,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) @@ -312,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 @@ -319,13 +375,16 @@ 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) { + GC.KeepAlive (this); return Array.Empty (); + } var count = (int)length; var coords = new int[count]; for (int i = 0; i < count; i++) coords[i] = ptr[i]; + GC.KeepAlive (this); return coords; } } @@ -334,12 +393,15 @@ 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) { + 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; } @@ -348,10 +410,14 @@ 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 () => + public void SetFunctionsOpenType () + { HarfBuzzApi.hb_ot_font_set_funcs (Handle); + GC.KeepAlive (this); + } public void Shape (Buffer buffer, params Feature[] features) => Shape (buffer, features, null); @@ -390,6 +456,9 @@ public void Shape (Buffer buffer, IReadOnlyList features, IReadOnlyList sPtr); } + GC.KeepAlive (buffer); + GC.KeepAlive (this); + 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..001ff28086b 100644 --- a/binding/HarfBuzzSharp/FontFunctions.cs +++ b/binding/HarfBuzzSharp/FontFunctions.cs @@ -21,9 +21,19 @@ 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); + GC.KeepAlive (this); + return r; + } + } - public void MakeImmutable () => HarfBuzzApi.hb_font_funcs_make_immutable (Handle); + public void MakeImmutable () + { + HarfBuzzApi.hb_font_funcs_make_immutable (Handle); + GC.KeepAlive (this); + } public void SetHorizontalFontExtentsDelegate (FontExtentsDelegate del, ReleaseDelegate destroy = null) { @@ -33,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) @@ -43,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) @@ -53,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) @@ -63,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) @@ -73,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) @@ -83,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) @@ -93,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) @@ -103,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) @@ -113,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) @@ -123,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) @@ -133,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) @@ -143,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) @@ -153,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) { @@ -162,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) @@ -172,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) @@ -182,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 459f86cfc88..7228a5be4c3 100644 --- a/binding/HarfBuzzSharp/UnicodeFunctions.cs +++ b/binding/HarfBuzzSharp/UnicodeFunctions.cs @@ -34,27 +34,55 @@ 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); + GC.KeepAlive (this); + return r; + } + } - public void MakeImmutable () => HarfBuzzApi.hb_unicode_funcs_make_immutable (Handle); + public void MakeImmutable () + { + HarfBuzzApi.hb_unicode_funcs_make_immutable (Handle); + GC.KeepAlive (this); + } 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return r; + } public bool TryCompose (int a, int b, out int ab) { @@ -68,7 +96,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -87,7 +117,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -98,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) @@ -107,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) @@ -116,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) @@ -125,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) @@ -134,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) @@ -143,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.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); } } diff --git a/binding/SkiaSharp/GRBackendRenderTarget.cs b/binding/SkiaSharp/GRBackendRenderTarget.cs index 0edf8eac0bf..7faee0e8164 100644 --- a/binding/SkiaSharp/GRBackendRenderTarget.cs +++ b/binding/SkiaSharp/GRBackendRenderTarget.cs @@ -91,12 +91,48 @@ 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); + 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; + } + } public SKSizeI Size => new SKSizeI (Width, Height); public SKRectI Rect => new SKRectI (0, 0, Width, Height); @@ -106,7 +142,9 @@ 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); + GC.KeepAlive (this); + return result; } } } diff --git a/binding/SkiaSharp/GRBackendTexture.cs b/binding/SkiaSharp/GRBackendTexture.cs index 531c4c6eb19..c58ca33e312 100644 --- a/binding/SkiaSharp/GRBackendTexture.cs +++ b/binding/SkiaSharp/GRBackendTexture.cs @@ -76,11 +76,41 @@ 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); + 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; + } + } public SKSizeI Size => new SKSizeI (Width, Height); public SKRectI Rect => new SKRectI (0, 0, Width, Height); @@ -90,7 +120,9 @@ 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); + GC.KeepAlive (this); + return result; } } } diff --git a/binding/SkiaSharp/GRContext.cs b/binding/SkiaSharp/GRContext.cs index 91aa9dc228c..1845a7b9264 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,13 @@ 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); + GC.KeepAlive (this); + return result; + } + } public void AbandonContext (bool releaseResources = false) { @@ -111,19 +121,28 @@ 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 () => - (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); + GC.KeepAlive (this); + return result; + } - public void SetResourceCacheLimit (long maxResourceBytes) => + 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) { IntPtr maxResBytes; fixed (int* maxRes = &maxResources) { SkiaApi.gr_direct_context_get_resource_cache_usage (Handle, maxRes, &maxResBytes); + GC.KeepAlive (this); } maxResourceBytes = (long)maxResBytes; } @@ -134,8 +153,11 @@ 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); + GC.KeepAlive (this); + } public void Flush () => Flush (true); @@ -145,10 +167,14 @@ 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) => + public void Submit (bool synchronous = false) + { SkiaApi.gr_direct_context_submit (Handle, synchronous); + GC.KeepAlive (this); + } public void Flush (SKImage image) { @@ -157,6 +183,8 @@ 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) @@ -166,25 +194,43 @@ 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) => 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); + GC.KeepAlive (this); + } - public void PurgeResources () => + public void PurgeResources () + { SkiaApi.gr_direct_context_free_gpu_resources (Handle); + GC.KeepAlive (this); + } - public void PurgeUnusedResources (long milliseconds) => + public void PurgeUnusedResources (long milliseconds) + { SkiaApi.gr_direct_context_perform_deferred_cleanup (Handle, milliseconds); + GC.KeepAlive (this); + } - public void PurgeUnlockedResources (bool scratchResourcesOnly) => + public void PurgeUnlockedResources (bool scratchResourcesOnly) + { SkiaApi.gr_direct_context_purge_unlocked_resources (Handle, scratchResourcesOnly); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } 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..f7fbb08cc5b 100644 --- a/binding/SkiaSharp/GRGlInterface.cs +++ b/binding/SkiaSharp/GRGlInterface.cs @@ -108,11 +108,19 @@ public static GRGlInterface CreateEvas (IntPtr evas) // - public bool Validate () => - SkiaApi.gr_glinterface_validate (Handle); + public bool Validate () + { + var result = SkiaApi.gr_glinterface_validate (Handle); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } // diff --git a/binding/SkiaSharp/GRRecordingContext.cs b/binding/SkiaSharp/GRRecordingContext.cs index 3a40694362d..14af9eed62e 100644 --- a/binding/SkiaSharp/GRRecordingContext.cs +++ b/binding/SkiaSharp/GRRecordingContext.cs @@ -11,16 +11,44 @@ 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 (); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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 ()); + GC.KeepAlive (this); + return result; + } internal static GRRecordingContext GetObject (IntPtr handle, bool owns = true, bool unrefExisting = true) { diff --git a/binding/SkiaSharp/HandleDictionary.cs b/binding/SkiaSharp/HandleDictionary.cs index 45ba20f87d6..494e3741592 100644 --- a/binding/SkiaSharp/HandleDictionary.cs +++ b/binding/SkiaSharp/HandleDictionary.cs @@ -56,6 +56,19 @@ 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, disposeProtected: false, objectFactory); + + /// + /// 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) where TSkiaObject : SKObject { if (handle == IntPtr.Zero) @@ -86,11 +99,22 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own refcnt.SafeUnRef (); } + if (disposeProtected) + // 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; } var obj = objectFactory.Invoke (handle, owns); + // Cannot race with a concurrent public Dispose call. same reasoning as above. + 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..1e96e188990 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,7 @@ class ReadWriteLock : IPlatformLock public void EnterUpgradeableReadLock () => _lock.EnterUpgradeableReadLock (); public void ExitUpgradeableReadLock () => _lock.ExitUpgradeableReadLock (); - ReaderWriterLockSlim _lock = new ReaderWriterLockSlim (); + ReaderWriterLockSlim _lock = new ReaderWriterLockSlim (LockRecursionPolicy.NoRecursion); } #if !(__IOS__ || __TVOS__ || __MACOS__ || __MACCATALYST__ || __ANDROID__) diff --git a/binding/SkiaSharp/SKBitmap.cs b/binding/SkiaSharp/SKBitmap.cs index cc4ed9f18c4..1a18d1216c8 100644 --- a/binding/SkiaSharp/SKBitmap.cs +++ b/binding/SkiaSharp/SKBitmap.cs @@ -81,13 +81,17 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } // Reset @@ -95,6 +99,7 @@ public bool TryAllocPixels (SKImageInfo info, SKBitmapAllocFlags flags) public void Reset () { SkiaApi.sk_bitmap_reset (Handle); + GC.KeepAlive (this); } // SetImmutable @@ -102,6 +107,7 @@ public void Reset () public void SetImmutable () { SkiaApi.sk_bitmap_set_immutable (Handle); + GC.KeepAlive (this); } // Erase @@ -109,23 +115,31 @@ 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 - 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } public void SetPixel (int x, int y, SKColor color) @@ -218,7 +232,10 @@ 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 (this); + GC.KeepAlive (destination); + return result; } // ExtractAlpha @@ -244,18 +261,29 @@ 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 (this); + 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); + GC.KeepAlive (this); + return result; + } + } public SKImageInfo Info { get { SKImageInfoNative cinfo; SkiaApi.sk_bitmap_get_info (Handle, &cinfo); + GC.KeepAlive (this); return SKImageInfoNative.ToManaged (ref cinfo); } } @@ -285,11 +313,19 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } } // *Pixels* @@ -306,13 +342,16 @@ 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); + GC.KeepAlive (this); + return result; } } public void SetPixels (IntPtr pixels) { SkiaApi.sk_bitmap_set_pixels (Handle, (void*)pixels); + GC.KeepAlive (this); } // more properties @@ -331,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; } @@ -365,7 +405,11 @@ public bool IsEmpty { } public bool IsNull { - get { return SkiaApi.sk_bitmap_is_null (Handle); } + get { + var result = SkiaApi.sk_bitmap_is_null (Handle); + GC.KeepAlive (this); + return result; + } } public bool DrawsNothing { @@ -373,7 +417,11 @@ public bool DrawsNothing { } public bool IsImmutable { - get { return SkiaApi.sk_bitmap_is_immutable (Handle); } + get { + var result = SkiaApi.sk_bitmap_is_immutable (Handle); + GC.KeepAlive (this); + return result; + } } // DecodeBounds @@ -612,12 +660,17 @@ 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); + GC.KeepAlive (this); + 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 (this); + GC.KeepAlive (pixmap); + return result; } // NotifyPixelsChanged @@ -625,6 +678,7 @@ public bool InstallPixels (SKPixmap pixmap) public void NotifyPixelsChanged () { SkiaApi.sk_bitmap_notify_pixels_changed (Handle); + GC.KeepAlive (this); } // PeekPixels @@ -647,6 +701,8 @@ 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; return result; @@ -756,6 +812,8 @@ 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); } // ToShader @@ -782,7 +840,11 @@ 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)); + GC.KeepAlive (this); + return result; + } } } diff --git a/binding/SkiaSharp/SKBlender.cs b/binding/SkiaSharp/SKBlender.cs index c0b5c811751..5379110c202 100644 --- a/binding/SkiaSharp/SKBlender.cs +++ b/binding/SkiaSharp/SKBlender.cs @@ -9,12 +9,7 @@ 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). var modes = new SKBlendMode[] { SKBlendMode.Clear, SKBlendMode.Src, @@ -48,18 +43,13 @@ 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) { + // 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); } } - internal static void EnsureStaticInstanceAreInitialized () - { - // 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) { @@ -81,15 +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)); - // - - private sealed class SKBlenderStatic : SKBlender - { - internal SKBlenderStatic (IntPtr x) - : base (x, false) - { - } - - protected override void Dispose (bool disposing) { } - } + 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/SKCanvas.cs b/binding/SkiaSharp/SKCanvas.cs index fee2faf9311..c20c54bd71f 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,19 @@ 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); + GC.KeepAlive (this); + } // QuickReject public bool QuickReject (SKRect rect) { - return SkiaApi.sk_canvas_quick_reject (Handle, &rect); + var result = SkiaApi.sk_canvas_quick_reject (Handle, &rect); + GC.KeepAlive (this); + return result; } public bool QuickReject (SKPath path) @@ -56,32 +62,56 @@ public int Save () { if (Handle == IntPtr.Zero) throw new ObjectDisposedException ("SKCanvas"); - return SkiaApi.sk_canvas_save (Handle); + var result = SkiaApi.sk_canvas_save (Handle); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } // DrawLine @@ -95,6 +125,8 @@ 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); + GC.KeepAlive (this); } // Clear @@ -102,22 +134,30 @@ 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); + GC.KeepAlive (this); + } - public void Clear (SKColorF color) => + public void Clear (SKColorF color) + { SkiaApi.sk_canvas_clear_color4f (Handle, color); + GC.KeepAlive (this); + } // Restore* 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 @@ -128,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) @@ -136,6 +177,7 @@ public void Translate (SKPoint point) return; SkiaApi.sk_canvas_translate (Handle, point.X, point.Y); + GC.KeepAlive (this); } // Scale @@ -146,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) @@ -154,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) @@ -162,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) @@ -182,6 +227,7 @@ public void RotateDegrees (float degrees) return; SkiaApi.sk_canvas_rotate_degrees (Handle, degrees); + GC.KeepAlive (this); } public void RotateRadians (float radians) @@ -190,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) @@ -220,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) @@ -228,6 +276,7 @@ public void Skew (SKPoint skew) return; SkiaApi.sk_canvas_skew (Handle, skew.X, skew.Y); + GC.KeepAlive (this); } // Concat @@ -240,6 +289,7 @@ public void Concat (in SKMatrix44 m) fixed (SKMatrix44* ptr = &m) { SkiaApi.sk_canvas_concat (Handle, ptr); } + GC.KeepAlive (this); } // Clip* @@ -247,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) @@ -255,6 +306,8 @@ 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); + GC.KeepAlive (this); } public void ClipPath (SKPath path, SKClipOperation operation = SKClipOperation.Intersect, bool antialias = false) @@ -263,6 +316,8 @@ 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); + GC.KeepAlive (this); } public void ClipRegion (SKRegion region, SKClipOperation operation = SKClipOperation.Intersect) @@ -271,6 +326,8 @@ 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); + GC.KeepAlive (this); } public SKRect LocalClipBounds { @@ -287,21 +344,37 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } } @@ -312,6 +385,8 @@ public void DrawPaint (SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_paint (Handle, paint.Handle); + GC.KeepAlive (paint); + GC.KeepAlive (this); } // DrawRegion @@ -323,6 +398,9 @@ 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); + GC.KeepAlive (this); } // DrawRect @@ -337,6 +415,8 @@ 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); + GC.KeepAlive (this); } // DrawRoundRect @@ -348,6 +428,9 @@ 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); + GC.KeepAlive (this); } public void DrawRoundRect (float x, float y, float w, float h, float rx, float ry, SKPaint paint) @@ -360,6 +443,8 @@ 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); + GC.KeepAlive (this); } public void DrawRoundRect (SKRect rect, SKSize r, SKPaint paint) @@ -384,6 +469,8 @@ 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); + GC.KeepAlive (this); } // DrawCircle @@ -393,6 +480,8 @@ 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); + GC.KeepAlive (this); } public void DrawCircle (SKPoint c, float radius, SKPaint paint) @@ -409,6 +498,9 @@ 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); + GC.KeepAlive (this); } // DrawPoints @@ -422,6 +514,8 @@ 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); + GC.KeepAlive (this); } // DrawPoint @@ -436,6 +530,8 @@ 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); + GC.KeepAlive (this); } public void DrawPoint (SKPoint p, SKColor color) @@ -472,6 +568,9 @@ 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); + GC.KeepAlive (this); } public void DrawImage (SKImage image, SKRect dest, SKPaint paint = null) @@ -499,6 +598,9 @@ 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); + GC.KeepAlive (this); } // DrawPicture @@ -520,6 +622,9 @@ 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); + GC.KeepAlive (this); } public void DrawPicture (SKPicture picture, SKPaint paint = null) @@ -527,6 +632,9 @@ 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); + GC.KeepAlive (this); } // DrawDrawable @@ -537,6 +645,8 @@ 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); + GC.KeepAlive (this); } public void DrawDrawable (SKDrawable drawable, float x, float y) @@ -616,6 +726,9 @@ 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); + GC.KeepAlive (this); } // DrawText @@ -715,15 +828,25 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } + } #nullable disable // Flush @@ -741,11 +864,15 @@ 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); + 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) @@ -758,6 +885,8 @@ 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); + GC.KeepAlive (this); } public SKData DrawNamedDestinationAnnotation (SKPoint point, string value) @@ -770,6 +899,8 @@ 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); + GC.KeepAlive (this); } public SKData DrawLinkDestinationAnnotation (SKRect rect, string value) @@ -802,6 +933,9 @@ 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); + GC.KeepAlive (this); } // Draw*Lattice @@ -867,6 +1001,9 @@ 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); + GC.KeepAlive (this); } // *Matrix @@ -874,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) => @@ -888,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; @@ -896,13 +1035,20 @@ public SKMatrix44 TotalMatrix44 { get { SKMatrix44 matrix; SkiaApi.sk_canvas_get_matrix (Handle, &matrix); + GC.KeepAlive (this); return matrix; } } // SaveCount - public int SaveCount => SkiaApi.sk_canvas_get_save_count (Handle); + public int SaveCount { + get { + var result = SkiaApi.sk_canvas_get_save_count (Handle); + GC.KeepAlive (this); + return result; + } + } // DrawVertices @@ -937,6 +1083,9 @@ 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); + GC.KeepAlive (this); } // DrawArc @@ -946,6 +1095,8 @@ 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); + GC.KeepAlive (this); } // DrawRoundRectDifference @@ -960,6 +1111,10 @@ 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); + GC.KeepAlive (this); } // DrawAtlas @@ -1001,6 +1156,9 @@ 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); + GC.KeepAlive (this); } // DrawPatch @@ -1029,6 +1187,8 @@ 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); + 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 859f8b7825a..c6b9db53376 100644 --- a/binding/SkiaSharp/SKCodec.cs +++ b/binding/SkiaSharp/SKCodec.cs @@ -29,27 +29,41 @@ public SKImageInfo Info { get { SKImageInfoNative cinfo; SkiaApi.sk_codec_get_info (Handle, &cinfo); + GC.KeepAlive (this); return SKImageInfoNative.ToManaged (ref cinfo); } } - public SKEncodedOrigin EncodedOrigin => - SkiaApi.sk_codec_get_origin (Handle); + public SKEncodedOrigin EncodedOrigin { + get { + var result = SkiaApi.sk_codec_get_origin (Handle); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } + } public SKSizeI GetScaledDimensions (float desiredScale) { SKSizeI dimensions; SkiaApi.sk_codec_get_scaled_dimensions (Handle, desiredScale, &dimensions); + GC.KeepAlive (this); return dimensions; } 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); + GC.KeepAlive (this); + return result; } } @@ -65,11 +79,21 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } + } public SKCodecFrameInfo[] FrameInfo { get { @@ -78,6 +102,7 @@ public SKCodecFrameInfo[] FrameInfo { fixed (SKCodecFrameInfo* i = info) { SkiaApi.sk_codec_get_frame_info (Handle, i); } + GC.KeepAlive (this); return info; } } @@ -85,7 +110,9 @@ 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); + GC.KeepAlive (this); + return result; } } @@ -133,7 +160,9 @@ 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); + GC.KeepAlive (this); + return result; } // incremental (start) @@ -156,13 +185,17 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } // incremental (step) @@ -170,12 +203,18 @@ 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); + GC.KeepAlive (this); + return result; } } - public SKCodecResult IncrementalDecode () => - SkiaApi.sk_codec_incremental_decode (Handle, null); + public SKCodecResult IncrementalDecode () + { + var result = SkiaApi.sk_codec_incremental_decode (Handle, null); + GC.KeepAlive (this); + return result; + } // scanline (start) @@ -194,13 +233,17 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } // scanline (step) @@ -210,19 +253,40 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } + } - public int NextScanline => SkiaApi.sk_codec_next_scanline (Handle); + public int NextScanline { + get { + var result = SkiaApi.sk_codec_next_scanline (Handle); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } // create (streams) @@ -270,7 +334,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 6abad3dd7d0..1f9c9a66fd1 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,25 +10,13 @@ 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 bool srgbToLinearInitialized; + private static object srgbToLinearLock = new object (); - 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 () - { - // IMPORTANT: do not remove to ensure that the static instances - // are initialized before any access is made to them - } + private static SKColorFilter linearToSrgb; + private static bool linearToSrgbInitialized; + private static object linearToSrgbLock = new object (); internal SKColorFilter(IntPtr handle, bool owns) : base (handle, owns) @@ -37,9 +26,15 @@ internal SKColorFilter(IntPtr handle, bool owns) protected override void Dispose (bool disposing) => base.Dispose (disposing); - public static SKColorFilter CreateSrgbToLinearGamma() => srgbToLinear; + public static SKColorFilter CreateSrgbToLinearGamma () => + LazyInitializer.EnsureInitialized ( + ref srgbToLinear, ref srgbToLinearInitialized, ref srgbToLinearLock, + () => GetDisposeProtectedObject (SkiaApi.sk_colorfilter_new_srgb_to_linear_gamma (), owns: false, unrefExisting: false)); - public static SKColorFilter CreateLinearToSrgbGamma() => linearToSrgb; + public static SKColorFilter CreateLinearToSrgbGamma () => + LazyInitializer.EnsureInitialized ( + ref linearToSrgb, ref linearToSrgbInitialized, ref linearToSrgbLock, + () => GetDisposeProtectedObject (SkiaApi.sk_colorfilter_new_linear_to_srgb_gamma (), owns: false, unrefExisting: false)); public static SKColorFilter CreateBlendMode(SKColor c, SKBlendMode mode) { @@ -57,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) @@ -65,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) @@ -158,15 +159,16 @@ 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) { } - } + // 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 aa994db762c..df9ad060812 100644 --- a/binding/SkiaSharp/SKColorSpace.cs +++ b/binding/SkiaSharp/SKColorSpace.cs @@ -2,55 +2,65 @@ 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 bool srgbInitialized; + private static object srgbLock = new object (); - 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 () - { - // IMPORTANT: do not remove to ensure that the static instances - // are initialized before any access is made to them - } + private static SKColorSpace srgbLinear; + private static bool srgbLinearInitialized; + private static object srgbLinearLock = new object (); internal SKColorSpace (IntPtr handle, bool owns) : base (handle, owns) { } - void ISKNonVirtualReferenceCounted.ReferenceNative () => + void ISKNonVirtualReferenceCounted.ReferenceNative () + { SkiaApi.sk_colorspace_ref (Handle); + GC.KeepAlive (this); + } - void ISKNonVirtualReferenceCounted.UnreferenceNative () => + void ISKNonVirtualReferenceCounted.UnreferenceNative () + { SkiaApi.sk_colorspace_unref (Handle); + GC.KeepAlive (this); + } 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } + } - public bool IsSrgb => - SkiaApi.sk_colorspace_is_srgb (Handle); + public bool IsSrgb { + get { + var result = SkiaApi.sk_colorspace_is_srgb (Handle); + GC.KeepAlive (this); + return result; + } + } public bool IsNumericalTransferFunction => GetNumericalTransferFunction (out _); @@ -62,16 +72,28 @@ 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 - public static SKColorSpace CreateSrgb () => srgb; + public static SKColorSpace CreateSrgb () => + LazyInitializer.EnsureInitialized ( + ref srgb, ref srgbInitialized, ref srgbLock, + // 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 () => srgbLinear; + public static SKColorSpace CreateSrgbLinear () => + LazyInitializer.EnsureInitialized ( + ref srgbLinear, ref srgbLinearInitialized, ref srgbLinearLock, + // 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 @@ -123,7 +145,9 @@ 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); + GC.KeepAlive (this); + return result; } } @@ -133,6 +157,7 @@ public SKColorSpaceIccProfile ToProfile () { var profile = new SKColorSpaceIccProfile (); SkiaApi.sk_colorspace_to_profile (Handle, profile.Handle); + GC.KeepAlive (this); return profile; } @@ -141,7 +166,9 @@ 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); + GC.KeepAlive (this); + return result; } } @@ -150,25 +177,29 @@ 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)); + GC.KeepAlive (this); + 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)); + GC.KeepAlive (this); + return result; + } // 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. 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 6be1ee9846e..c0627b12184 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,23 +16,9 @@ public unsafe class SKData : SKObject, ISKNonVirtualReferenceCounted // improvement in Copy performance. internal const int CopyBufferSize = 81920; - private static readonly 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 () - { - // IMPORTANT: do not remove to ensure that the static instances - // are initialized before any access is made to them - } + private static SKData empty; + private static bool emptyInitialized; + private static object emptyLock = new object (); internal SKData (IntPtr x, bool owns) : base (x, owns) @@ -41,11 +28,24 @@ 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); + GC.KeepAlive (this); + } - void ISKNonVirtualReferenceCounted.UnreferenceNative () => SkiaApi.sk_data_unref (Handle); + void ISKNonVirtualReferenceCounted.UnreferenceNative () + { + SkiaApi.sk_data_unref (Handle); + GC.KeepAlive (this); + } - public static SKData Empty => empty; + public static SKData Empty => + LazyInitializer.EnsureInitialized ( + ref empty, ref emptyInitialized, ref emptyLock, + // 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 @@ -228,7 +228,9 @@ 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)); + GC.KeepAlive (this); + return result; } // ToArray @@ -244,9 +246,21 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } + } public Span Span => new Span ((void*)Data, (int)Size); @@ -288,6 +302,9 @@ 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, bool owns = true, bool unrefExisting = true) => + GetOrAddDisposeProtectedObject (handle, owns, unrefExisting, (h, o) => new SKData (h, o)); + // private class SKDataStream : UnmanagedMemoryStream @@ -313,16 +330,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/SKDocument.cs b/binding/SkiaSharp/SKDocument.cs index eb2d179812a..e73b05c9428 100644 --- a/binding/SkiaSharp/SKDocument.cs +++ b/binding/SkiaSharp/SKDocument.cs @@ -18,20 +18,37 @@ 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (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); + GC.KeepAlive (this); + return result; + } - public void EndPage () => + public void EndPage () + { SkiaApi.sk_document_end_page (Handle); + GC.KeepAlive (this); + } - public void Close () => + 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 c23cf0a04f8..2d089be0a7d 100644 --- a/binding/SkiaSharp/SKDrawable.cs +++ b/binding/SkiaSharp/SKDrawable.cs @@ -54,23 +54,37 @@ 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); + GC.KeepAlive (this); + return result; + } + } public SKRect Bounds { get { SKRect bounds; SkiaApi.sk_drawable_get_bounds (Handle, &bounds); + GC.KeepAlive (this); return 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); } public void Draw (SKCanvas canvas, float x, float y) @@ -80,11 +94,18 @@ 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); + GC.KeepAlive (this); + return result; + } - public void NotifyDrawingChanged () => + 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 71beb79b96f..cd3dc0cb881 100644 --- a/binding/SkiaSharp/SKFont.cs +++ b/binding/SkiaSharp/SKFont.cs @@ -25,77 +25,171 @@ 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); + GC.KeepAlive (this); + } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_font_set_force_auto_hinting (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_font_set_embedded_bitmaps (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_font_set_subpixel (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_font_set_linear_metrics (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_font_set_embolden (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_font_set_baseline_snap (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_font_set_edging (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_font_set_hinting (Handle, value); + GC.KeepAlive (this); + } } 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)); + 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 => SkiaApi.sk_font_get_size (Handle); - set => SkiaApi.sk_font_set_size (Handle, value); + 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 => 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_font_set_scale_x (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_font_set_skew_x (Handle, value); + GC.KeepAlive (this); + } } // FontSpacing - public float Spacing => - SkiaApi.sk_font_get_metrics (Handle, null); + public float Spacing { + get { + var r = SkiaApi.sk_font_get_metrics (Handle, null); + GC.KeepAlive (this); + return r; + } + } // FontMetrics @@ -109,14 +203,20 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return r; + } // GetGlyphs @@ -138,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); } } @@ -204,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); } } @@ -563,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); } } @@ -707,6 +818,8 @@ 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); + GC.KeepAlive (this); } // GetGlyphPath @@ -718,6 +831,7 @@ public SKPath GetGlyphPath (ushort glyph) path.Dispose (); path = null; } + GC.KeepAlive (this); return path; } @@ -750,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; } @@ -783,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; } @@ -796,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 184da670674..1a15f2552c8 100644 --- a/binding/SkiaSharp/SKFontManager.cs +++ b/binding/SkiaSharp/SKFontManager.cs @@ -5,28 +5,15 @@ 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; - - 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 () - { - // IMPORTANT: do not remove to ensure that the static instances - // are initialized before any access is made to them - } + private static SKFontManager defaultManager; + private static bool defaultManagerInitialized; + private static object defaultManagerLock = new object (); internal SKFontManager (IntPtr handle, bool owns) : base (handle, owns) @@ -36,9 +23,18 @@ internal SKFontManager (IntPtr handle, bool owns) protected override void Dispose (bool disposing) => base.Dispose (disposing); - public static SKFontManager Default => defaultManager; + 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); + public int FontFamilyCount { + get { + var r = SkiaApi.sk_fontmgr_count_families (Handle); + GC.KeepAlive (this); + return r; + } + } public IEnumerable FontFamilies { get { @@ -53,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; } @@ -60,14 +57,18 @@ 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)); + GC.KeepAlive (this); + 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))); + GC.KeepAlive (this); + return styleSet; } } @@ -80,9 +81,10 @@ 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; + var typeface = SKTypeface.GetDisposeProtectedObject (SkiaApi.sk_fontmgr_match_family_style (Handle, new IntPtr (familyNamePointer), style.Handle)); + GC.KeepAlive (style); + GC.KeepAlive (this); + return typeface; } } @@ -93,7 +95,9 @@ 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)); + GC.KeepAlive (this); + return typeface; } } @@ -116,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,7 +130,10 @@ 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); + GC.KeepAlive (this); + return typeface; } public SKTypeface MatchCharacter (char character) @@ -184,9 +192,10 @@ 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; + 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; } } @@ -200,16 +209,8 @@ public static SKFontManager CreateDefault () internal static SKFontManager GetObject (IntPtr handle) => GetOrAddObject (handle, (h, o) => new SKFontManager (h, o)); - // - - private sealed class SKFontManagerStatic : SKFontManager - { - internal SKFontManagerStatic (IntPtr x) - : base (x, false) - { - } + internal static SKFontManager GetDisposeProtectedObject (IntPtr handle) => + GetOrAddDisposeProtectedObject (handle, owns: true, unrefExisting: true, (h, o) => new SKFontManager (h, o)); - protected override void Dispose (bool disposing) { } - } } } diff --git a/binding/SkiaSharp/SKFontStyle.cs b/binding/SkiaSharp/SKFontStyle.cs index c1b9ef93c2c..a3866eddda7 100644 --- a/binding/SkiaSharp/SKFontStyle.cs +++ b/binding/SkiaSharp/SKFontStyle.cs @@ -6,17 +6,23 @@ 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; - - static SKFontStyle () + private static readonly SKFontStyle normal = + MakeDisposeProtected (SKFontStyleWeight.Normal, SKFontStyleSlant.Upright); + private static readonly SKFontStyle bold = + MakeDisposeProtected (SKFontStyleWeight.Bold, SKFontStyleSlant.Upright); + private static readonly SKFontStyle italic = + MakeDisposeProtected (SKFontStyleWeight.Normal, SKFontStyleSlant.Italic); + private static readonly SKFontStyle boldItalic = + MakeDisposeProtected (SKFontStyleWeight.Bold, SKFontStyleSlant.Italic); + + private static SKFontStyle MakeDisposeProtected (SKFontStyleWeight weight, SKFontStyleSlant slant) { - 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); + 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; } internal SKFontStyle (IntPtr handle, bool owns) @@ -63,15 +69,5 @@ protected override void DisposeNative () => 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/SKFontStyleSet.cs b/binding/SkiaSharp/SKFontStyleSet.cs index 7815b2c1297..9d1a80fc80a 100644 --- a/binding/SkiaSharp/SKFontStyleSet.cs +++ b/binding/SkiaSharp/SKFontStyleSet.cs @@ -21,7 +21,13 @@ 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); + GC.KeepAlive (this); + return r; + } + } public SKFontStyle this[int index] => GetStyle (index); @@ -38,8 +44,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 +54,8 @@ 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 (style); GC.KeepAlive (this); return tf; } @@ -71,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/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..483166e10a6 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,69 @@ 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); + GC.KeepAlive (this); + return result; + } + } - public int Height => - SkiaApi.sk_image_get_height (Handle); + public int Height { + get { + var result = SkiaApi.sk_image_get_height (Handle); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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 (); + GC.KeepAlive (this); + 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)); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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)); + GC.KeepAlive (this); + return result; + } + } public SKImageInfo Info => new SKImageInfo (Width, Height, ColorType, AlphaType, ColorSpace); @@ -420,8 +480,12 @@ 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)); + GC.KeepAlive (this); + return result; + } // ToRawShader @@ -440,8 +504,12 @@ 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)); + GC.KeepAlive (this); + return result; + } // PeekPixels @@ -451,6 +519,8 @@ 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; return result; @@ -466,17 +536,32 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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 (this); + GC.KeepAlive (context); + return result; + } // ReadPixels @@ -510,6 +595,7 @@ public bool ReadPixels (SKPixmap pixmap, int srcX, int srcY, SKImageCachingHint 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 +618,27 @@ 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 (this); + 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)); + GC.KeepAlive (this); + 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 (this); + GC.KeepAlive (context); + return image; } // ToRasterImage @@ -552,10 +646,14 @@ 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)); + GC.KeepAlive (this); + return image; + } // ToTextureImage @@ -570,7 +668,10 @@ 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 (this); + GC.KeepAlive (context); + return image; } // ApplyImageFilter @@ -589,7 +690,10 @@ 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 (this); + GC.KeepAlive (filter); + return image; } } @@ -603,7 +707,11 @@ 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 (this); + 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..504716bf392 100644 --- a/binding/SkiaSharp/SKNWayCanvas.cs +++ b/binding/SkiaSharp/SKNWayCanvas.cs @@ -23,6 +23,8 @@ public void AddCanvas (SKCanvas canvas) throw new ArgumentNullException (nameof (canvas)); SkiaApi.sk_nway_canvas_add_canvas (Handle, canvas.Handle); + GC.KeepAlive (canvas); + GC.KeepAlive (this); } public void RemoveCanvas (SKCanvas canvas) @@ -31,11 +33,14 @@ public void RemoveCanvas (SKCanvas canvas) throw new ArgumentNullException (nameof (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/SKObject.cs b/binding/SkiaSharp/SKObject.cs index a8920802513..f578ba4257b 100644 --- a/binding/SkiaSharp/SKObject.cs +++ b/binding/SkiaSharp/SKObject.cs @@ -36,18 +36,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) : base (handle, owns) { @@ -124,6 +112,22 @@ internal static TSkiaObject GetOrAddObject (IntPtr handle, bool own return HandleDictionary.GetOrAddObject (handle, owns, unrefExisting, 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, disposeProtected: true, objectFactory); + } + internal static void RegisterHandle (IntPtr handle, SKObject instance) { if (handle == IntPtr.Zero || instance == null) @@ -151,25 +155,6 @@ 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; - - 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) @@ -207,12 +192,40 @@ 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 { internal bool fromFinalizer = false; + // 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) @@ -230,6 +243,10 @@ internal SKNativeObject (IntPtr handle, bool ownsHandle) { fromFinalizer = true; + // 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; Dispose (false); } @@ -237,9 +254,47 @@ internal SKNativeObject (IntPtr handle, bool ownsHandle) protected internal virtual bool OwnsHandle { get; protected set; } - protected internal bool IgnorePublicDispose { get; set; } + // One-way latch: once set to true, only stays true. Use PreventPublicDisposal() to set. + // 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. + // 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; + } - 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 () { @@ -256,11 +311,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 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) { - if (Interlocked.CompareExchange (ref isDisposed, 1, 0) != 0) - return; - // dispose any objects that are owned/created by native code if (disposing) DisposeUnownedManaged (); @@ -278,17 +335,69 @@ protected virtual void Dispose (bool disposing) public void Dispose () { - if (IgnorePublicDispose) + // 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. + // + // 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 (); +#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 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) + return; Dispose (true); GC.SuppressFinalize (this); } + } internal static class SKObjectExtensions 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 a100d51a930..066c11585db 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,17 +44,27 @@ 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; - - static SKPaint () - { - defaultFont = new SKFontStatic ( - SkiaApi.sk_font_new_with_values ( - SKTypeface.Default.Handle, - SKFont.DefaultSize, - SKFont.DefaultScaleX, - SKFont.DefaultSkewX)); - } + private static SKFont defaultFont; + 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, + SKFont.DefaultSize, + 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; + }); internal SKPaint (IntPtr handle, bool owns) : base (handle, owns) @@ -61,7 +72,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."); @@ -76,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."); @@ -84,31 +96,44 @@ 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); + GC.KeepAlive (this); + } // Reset - public void Reset () => - SkiaApi.sk_compatpaint_reset (Handle, defaultFont.Handle); - - private sealed class SKFontStatic : SKFont + public void Reset () { - internal SKFontStatic (IntPtr handle) : base (handle, false) { } - - protected override void Dispose (bool disposing) { } + SkiaApi.sk_compatpaint_reset (Handle, DefaultFont.Handle); + GC.KeepAlive (this); } // 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_compatpaint_set_is_antialias (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_paint_set_dither (Handle, value); + GC.KeepAlive (this); + } } [Obsolete ($"Use {nameof (SKFont)}.{nameof (SKFont.LinearMetrics)} instead.", error: true)] @@ -125,8 +150,15 @@ 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_compatpaint_set_lcd_render_text (Handle, value); + GC.KeepAlive (this); + } } [Obsolete ($"Use {nameof (SKFont)}.{nameof (SKFont.EmbeddedBitmaps)} instead.", error: true)] @@ -159,81 +191,185 @@ 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_paint_set_style (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_paint_set_color (Handle, (uint)value); + GC.KeepAlive (this); + } } 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); + set { + SkiaApi.sk_paint_set_color4f (Handle, &value, IntPtr.Zero); + GC.KeepAlive (this); + } } - 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); + GC.KeepAlive (this); + } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_paint_set_stroke_width (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_paint_set_stroke_miter (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_paint_set_stroke_cap (Handle, value); + GC.KeepAlive (this); + } } 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_paint_set_stroke_join (Handle, value); + GC.KeepAlive (this); + } } 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)); + 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 => 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)); + 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 => 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)); + 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 => 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)); + 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 => SkiaApi.sk_paint_get_blendmode (Handle); - set => SkiaApi.sk_paint_set_blendmode (Handle, value); + 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 => 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)); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_paint_set_blender (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + GC.KeepAlive (this); + } } [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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_compatpaint_set_filter_quality (Handle, (int)value); + GC.KeepAlive (this); + } } [Obsolete ($"Use {nameof (SKFont)}.{nameof (SKFont.Typeface)} instead.", error: true)] @@ -250,14 +386,28 @@ 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_compatpaint_set_text_align (Handle, value); + GC.KeepAlive (this); + } } [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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_compatpaint_set_text_encoding (Handle, value); + GC.KeepAlive (this); + } } [Obsolete ($"Use {nameof (SKFont)}.{nameof (SKFont.ScaleX)} instead.", error: true)] @@ -273,8 +423,16 @@ 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)); + 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); + } } // FontSpacing @@ -298,8 +456,12 @@ 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))!; + GC.KeepAlive (this); + return r; + } // MeasureText @@ -564,7 +726,11 @@ 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); + GC.KeepAlive (this); + return result; } // CountGlyphs @@ -859,8 +1025,12 @@ 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)); + GC.KeepAlive (this); + 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..3f06ec86f7b 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,9 +69,14 @@ protected override void DisposeNative () } public SKPathFillType FillType { - get => SkiaApi.sk_path_get_filltype (Handle); + 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; } @@ -78,25 +84,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); @@ -106,6 +112,7 @@ public SKPoint LastPoint { get { SKPoint point; SkiaApi.sk_path_get_last_point (Handle, &point); + GC.KeepAlive (this); return point; } } @@ -114,6 +121,7 @@ public SKRect Bounds { get { SKRect rect; SkiaApi.sk_path_get_bounds (Handle, &rect); + GC.KeepAlive (this); return rect; } } @@ -131,7 +139,9 @@ 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); + GC.KeepAlive (this); + if (isOval) { return bounds; } else { return SKRect.Empty; @@ -142,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 { @@ -155,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 { @@ -172,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; @@ -188,6 +201,7 @@ public SKPoint GetPoint (int index) SKPoint point; SkiaApi.sk_path_get_point (Handle, index, &point); + GC.KeepAlive (this); return point; } @@ -201,13 +215,17 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return r; } public void Offset (SKPoint offset) => @@ -226,6 +244,7 @@ public void Reset () _builder = null; } SkiaApi.sk_path_reset (Handle); + GC.KeepAlive (this); } public bool GetBounds (out SKRect rect) @@ -236,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; @@ -245,13 +265,16 @@ public SKRect ComputeTightBounds () { SKRect rect; SkiaApi.sk_path_compute_tight_bounds (Handle, &rect); + GC.KeepAlive (this); return rect; } public void Transform (in SKMatrix matrix) { - fixed (SKMatrix* m = &matrix) + fixed (SKMatrix* m = &matrix) { SkiaApi.sk_path_transform (Handle, m); + GC.KeepAlive (this); + } } public void Transform (in SKMatrix matrix, SKPath destination) @@ -261,6 +284,8 @@ 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)] @@ -288,7 +313,11 @@ 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); + GC.KeepAlive (this); + return success; } public SKPath Op (SKPath other, SKPathOp op) @@ -307,7 +336,10 @@ 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); + GC.KeepAlive (this); + return success; } public SKPath Simplify () @@ -324,7 +356,9 @@ 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); + GC.KeepAlive (this); + return success; } } @@ -333,7 +367,10 @@ 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); + GC.KeepAlive (this); + return success; } public SKPath ToWinding () @@ -351,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; } @@ -410,6 +448,7 @@ private void FlushBuilder () _builder.Dispose (); _builder = null; SkiaApi.sk_path_delete (Handle); + GC.KeepAlive (this); Handle = newHandle; } @@ -420,7 +459,9 @@ internal void ReplaceFromBuilder (SKPathBuilder builder) _builder = null; } var newHandle = SkiaApi.sk_pathbuilder_detach_path (builder.Handle); + GC.KeepAlive (builder); SkiaApi.sk_path_delete (Handle); + GC.KeepAlive (this); Handle = newHandle; } @@ -782,18 +823,32 @@ 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); + GC.KeepAlive (this); + return r; } } - public float ConicWeight () => - SkiaApi.sk_path_iter_conic_weight (Handle); + public float ConicWeight () + { + var r = SkiaApi.sk_path_iter_conic_weight (Handle); + GC.KeepAlive (this); + 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; + GC.KeepAlive (this); + 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; + GC.KeepAlive (this); + return r; + } } public class RawIterator : SKObject, ISKSkipObjectRegistration @@ -820,15 +875,25 @@ 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); + GC.KeepAlive (this); + return r; } } - public float ConicWeight () => - SkiaApi.sk_path_rawiter_conic_weight (Handle); + public float ConicWeight () + { + var r = SkiaApi.sk_path_rawiter_conic_weight (Handle); + GC.KeepAlive (this); + return r; + } - public SKPathVerb Peek () => - SkiaApi.sk_path_rawiter_peek (Handle); + public SKPathVerb Peek () + { + var r = SkiaApi.sk_path_rawiter_peek (Handle); + GC.KeepAlive (this); + return r; + } } public class OpBuilder : SKObject, ISKSkipObjectRegistration @@ -838,15 +903,22 @@ 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); + GC.KeepAlive (this); + } 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); + GC.KeepAlive (this); + return success; } protected override void Dispose (bool disposing) => diff --git a/binding/SkiaSharp/SKPathBuilder.cs b/binding/SkiaSharp/SKPathBuilder.cs index 3a4b1be94bb..b33c5c99528 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,226 @@ 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); + GC.KeepAlive (this); + return r; + } + set { + SkiaApi.sk_pathbuilder_set_filltype (Handle, value); + GC.KeepAlive (this); + } } - public SKPath Detach () => - SKPath.GetObject (SkiaApi.sk_pathbuilder_detach_path (Handle)); + public SKPath Detach () + { + var r = SKPath.GetObject (SkiaApi.sk_pathbuilder_detach_path (Handle)); + GC.KeepAlive (this); + 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)); + GC.KeepAlive (this); + return r; + } - public void Reset () => + public void Reset () + { SkiaApi.sk_pathbuilder_reset (Handle); + GC.KeepAlive (this); + } // Move - public void MoveTo (SKPoint point) => + 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) => + public void MoveTo (float x, float y) + { SkiaApi.sk_pathbuilder_move_to (Handle, x, y); + GC.KeepAlive (this); + } - public void RMoveTo (SKPoint point) => + 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) => + public void RMoveTo (float dx, float dy) + { SkiaApi.sk_pathbuilder_rmove_to (Handle, dx, dy); + GC.KeepAlive (this); + } // Line - public void LineTo (SKPoint point) => + 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) => + public void LineTo (float x, float y) + { SkiaApi.sk_pathbuilder_line_to (Handle, x, y); + GC.KeepAlive (this); + } - public void RLineTo (SKPoint point) => + 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) => + public void RLineTo (float dx, float dy) + { SkiaApi.sk_pathbuilder_rline_to (Handle, dx, dy); + GC.KeepAlive (this); + } // 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } // 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } // 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } // 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } // Close - public void Close () => + public void Close () + { SkiaApi.sk_pathbuilder_close (Handle); + GC.KeepAlive (this); + } // 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); + GC.KeepAlive (this); + } public void AddRect (SKRect rect, SKPathDirection direction, uint startIndex) { @@ -156,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) @@ -163,6 +270,8 @@ 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); + GC.KeepAlive (this); } public void AddRoundRect (SKRoundRect rect, SKPathDirection direction, uint startIndex) @@ -170,24 +279,39 @@ 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); + GC.KeepAlive (this); } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + GC.KeepAlive (this); + } - 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); + 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); } } @@ -197,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); } } @@ -208,6 +333,8 @@ 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); + GC.KeepAlive (this); } public void AddPath (SKPath other, in SKMatrix matrix, SKPathAddMode mode = SKPathAddMode.Append) @@ -217,6 +344,8 @@ 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) @@ -225,6 +354,8 @@ 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); + GC.KeepAlive (this); } public void ReverseAddPath (SKPath other) @@ -233,6 +364,8 @@ public void ReverseAddPath (SKPath other) throw new ArgumentNullException (nameof (other)); SkiaApi.sk_pathbuilder_reverse_add_path (Handle, other.Handle); + GC.KeepAlive (other); + GC.KeepAlive (this); } } } 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..5a03e8980f6 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,17 @@ protected override void DisposeNative () => public float Length { get { - return SkiaApi.sk_pathmeasure_get_length (Handle); + var r = SkiaApi.sk_pathmeasure_get_length (Handle); + GC.KeepAlive (this); + return r; } } public bool IsClosed { get { - return SkiaApi.sk_pathmeasure_is_closed (Handle); + var r = SkiaApi.sk_pathmeasure_is_closed (Handle); + GC.KeepAlive (this); + return r; } } @@ -60,6 +65,8 @@ 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); + GC.KeepAlive (this); } // GetPositionAndTangent @@ -68,7 +75,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -84,7 +93,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -100,7 +111,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -116,7 +129,9 @@ 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); + GC.KeepAlive (this); + return r; } } @@ -126,7 +141,10 @@ 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); + GC.KeepAlive (this); + return result; } [Obsolete ("Use the SKPathBuilder overload instead.")] @@ -156,7 +174,9 @@ 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); + GC.KeepAlive (this); + return r; } } } diff --git a/binding/SkiaSharp/SKPicture.cs b/binding/SkiaSharp/SKPicture.cs index 4726dcebbc2..3ccdb97eeab 100644 --- a/binding/SkiaSharp/SKPicture.cs +++ b/binding/SkiaSharp/SKPicture.cs @@ -15,30 +15,49 @@ 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); + GC.KeepAlive (this); + return result; + } + } public SKRect CullRect { get { SKRect rect; SkiaApi.sk_picture_get_cull_rect (Handle, &rect); + GC.KeepAlive (this); return rect; } } - 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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)); + GC.KeepAlive (this); + return result; + } public void Serialize (Stream stream) { @@ -55,6 +74,8 @@ public void Serialize (SKWStream stream) throw new ArgumentNullException (nameof (stream)); SkiaApi.sk_picture_serialize_to_stream (Handle, stream.Handle); + GC.KeepAlive (stream); + GC.KeepAlive (this); } // Playback @@ -65,6 +86,8 @@ public void Playback (SKCanvas canvas) throw new ArgumentNullException (nameof (canvas)); SkiaApi.sk_picture_playback (Handle, canvas.Handle); + GC.KeepAlive (canvas); + GC.KeepAlive (this); } // ToShader @@ -90,8 +113,12 @@ 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)); + GC.KeepAlive (this); + return result; + } // Deserialize @@ -121,7 +148,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 +167,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..c33ce303914 100644 --- a/binding/SkiaSharp/SKPictureRecorder.cs +++ b/binding/SkiaSharp/SKPictureRecorder.cs @@ -27,7 +27,9 @@ 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); + GC.KeepAlive (this); + return result; } public SKCanvas BeginRecording (SKRect cullRect, bool useRTree) @@ -41,7 +43,9 @@ 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); + GC.KeepAlive (this); + return result; } finally { if (rtreeHandle != IntPtr.Zero) { SkiaApi.sk_rtree_factory_delete (rtreeHandle); @@ -51,15 +55,24 @@ 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)); + GC.KeepAlive (this); + 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)); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } + } } } diff --git a/binding/SkiaSharp/SKPixmap.cs b/binding/SkiaSharp/SKPixmap.cs index 7ec88b841ec..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); } } @@ -93,14 +96,25 @@ 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)); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } + } public int BytesSize => Info.BytesSize; @@ -108,11 +122,19 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } public Span GetPixelSpan () => GetPixelSpan (0, 0); @@ -163,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) @@ -171,18 +194,27 @@ 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); + GC.KeepAlive (this); + return result; + } 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) => - 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); + GC.KeepAlive (this); + return result; + } // ScalePixels @@ -196,7 +228,10 @@ 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 (this); + GC.KeepAlive (destination); + return result; } // ReadPixels @@ -204,7 +239,9 @@ 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); + GC.KeepAlive (this); + return result; } public bool ReadPixels (SKImageInfo dstInfo, IntPtr dstPixels, int dstRowBytes) => @@ -264,7 +301,10 @@ 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 (this); + GC.KeepAlive (dst); + return result; } // Encode (jpeg) @@ -286,7 +326,10 @@ 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 (this); + GC.KeepAlive (dst); + return result; } // Encode (png) @@ -308,7 +351,10 @@ 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 (this); + GC.KeepAlive (dst); + return result; } // ExtractSubset @@ -326,7 +372,10 @@ 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 (this); + GC.KeepAlive (result); + return extracted; } // Erase @@ -334,19 +383,31 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } // With* diff --git a/binding/SkiaSharp/SKRegion.cs b/binding/SkiaSharp/SKRegion.cs index dfb835583e8..0c1d4457cc9 100644 --- a/binding/SkiaSharp/SKRegion.cs +++ b/binding/SkiaSharp/SKRegion.cs @@ -42,19 +42,35 @@ 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); + GC.KeepAlive (this); + return result; + } + } - public bool IsRect => - SkiaApi.sk_region_is_rect (Handle); + public bool IsRect { + get { + var result = SkiaApi.sk_region_is_rect (Handle); + GC.KeepAlive (this); + return result; + } + } - public bool IsComplex => - SkiaApi.sk_region_is_complex (Handle); + public bool IsComplex { + get { + var result = SkiaApi.sk_region_is_complex (Handle); + GC.KeepAlive (this); + return result; + } + } public SKRectI Bounds { get { SKRectI rect; SkiaApi.sk_region_get_bounds (Handle, &rect); + GC.KeepAlive (this); return rect; } } @@ -68,6 +84,7 @@ public SKPath GetBoundaryPath () path.Dispose (); path = null; } + GC.KeepAlive (this); return path; } @@ -87,34 +104,60 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } public bool QuickReject (SKPath path) @@ -142,32 +185,51 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } // Set* - public void SetEmpty () => + public void SetEmpty () + { SkiaApi.sk_region_set_empty (Handle); + GC.KeepAlive (this); + } 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } } @@ -178,7 +240,11 @@ 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); + GC.KeepAlive (this); + return result; } public bool SetPath (SKPath path) @@ -191,24 +257,39 @@ 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); + GC.KeepAlive (this); + return result; } // Translate - public void Translate (int x, int y) => + public void Translate (int x, int y) + { SkiaApi.sk_region_translate (Handle, x, y); + GC.KeepAlive (this); + } // 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; + } public bool Op (SKPath path, SKRegionOperation op) { @@ -246,6 +327,7 @@ public bool Next (out SKRectI rect) { if (SkiaApi.sk_region_iterator_done (Handle)) { rect = SKRectI.Empty; + GC.KeepAlive (this); return false; } @@ -254,6 +336,7 @@ public bool Next (out SKRectI rect) } SkiaApi.sk_region_iterator_next (Handle); + GC.KeepAlive (this); return true; } @@ -278,6 +361,7 @@ public bool Next (out SKRectI rect) { if (SkiaApi.sk_region_cliperator_done (Handle)) { rect = SKRectI.Empty; + GC.KeepAlive (this); return false; } @@ -286,6 +370,7 @@ public bool Next (out SKRectI rect) } SkiaApi.sk_region_cliperator_next (Handle); + GC.KeepAlive (this); return true; } @@ -308,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 67f8794da4f..05412b91a77 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) => @@ -58,6 +59,7 @@ public SKRect Rect { get { SKRect rect; SkiaApi.sk_rrect_get_rect (Handle, &rect); + GC.KeepAlive (this); return rect; } } @@ -69,13 +71,37 @@ 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); + GC.KeepAlive (this); + return r; + } + } - public float Width => SkiaApi.sk_rrect_get_width (Handle); + public float Width { + get { + var r = SkiaApi.sk_rrect_get_width (Handle); + GC.KeepAlive (this); + return r; + } + } - public float Height => SkiaApi.sk_rrect_get_height (Handle); + public float Height { + get { + var r = SkiaApi.sk_rrect_get_height (Handle); + GC.KeepAlive (this); + return r; + } + } - public bool IsValid => SkiaApi.sk_rrect_is_valid (Handle); + public bool IsValid { + get { + var r = SkiaApi.sk_rrect_is_valid (Handle); + GC.KeepAlive (this); + return r; + } + } public bool AllCornersCircular => CheckAllCornersCircular (Utils.NearlyZero); @@ -96,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) @@ -133,18 +164,22 @@ 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) { - return SkiaApi.sk_rrect_contains (Handle, &rect); + var r = SkiaApi.sk_rrect_contains (Handle, &rect); + GC.KeepAlive (this); + return r; } public SKPoint GetRadii (SKRoundRectCorner corner) { SKPoint radii; SkiaApi.sk_rrect_get_radii (Handle, corner, &radii); + GC.KeepAlive (this); return radii; } @@ -156,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) @@ -166,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) @@ -176,12 +213,15 @@ 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 (); - if (SkiaApi.sk_rrect_transform (Handle, &matrix, destHandle)) { + 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 9a265379387..b98a010ecce 100644 --- a/binding/SkiaSharp/SKRuntimeEffect.cs +++ b/binding/SkiaSharp/SKRuntimeEffect.cs @@ -87,8 +87,13 @@ 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); + GC.KeepAlive (this); + return size; + } + } public IReadOnlyList Children => children ??= GetChildrenNames ().ToArray (); @@ -106,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 () @@ -116,6 +122,7 @@ private IEnumerable GetUniformNames () SkiaApi.sk_runtimeeffect_get_uniform_name (Handle, i, str.Handle); yield return str.ToString (); } + GC.KeepAlive (this); } // ToShader @@ -138,7 +145,10 @@ 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); + GC.KeepAlive (this); + return shader; } } @@ -162,7 +172,10 @@ 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); + GC.KeepAlive (this); + return colorFilter; } } @@ -186,7 +199,10 @@ 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); + GC.KeepAlive (this); + return blender; } } diff --git a/binding/SkiaSharp/SKShader.cs b/binding/SkiaSharp/SKShader.cs index 93b676c0bc6..85566572135 100644 --- a/binding/SkiaSharp/SKShader.cs +++ b/binding/SkiaSharp/SKShader.cs @@ -22,13 +22,20 @@ 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); + GC.KeepAlive (this); + 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)); + GC.KeepAlive (this); + return shader; + } // CreateEmpty @@ -45,7 +52,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 +172,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 +188,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 +237,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 +252,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 +319,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 +334,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 +383,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 +398,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 +438,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 +450,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 +461,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..3529b0f683a 100644 --- a/binding/SkiaSharp/SKStream.cs +++ b/binding/SkiaSharp/SKStream.cs @@ -14,7 +14,9 @@ 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); + GC.KeepAlive (this); + return result; } } @@ -70,42 +72,54 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } } @@ -113,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; } @@ -126,27 +141,37 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } public bool Rewind () { - return SkiaApi.sk_stream_rewind (Handle); + var result = SkiaApi.sk_stream_rewind (Handle); + GC.KeepAlive (this); + return result; } public bool Seek (int position) { - return SkiaApi.sk_stream_seek (Handle, (IntPtr)position); + var result = SkiaApi.sk_stream_seek (Handle, (IntPtr)position); + GC.KeepAlive (this); + return result; } [Obsolete ("The native stream move offset is capped at a 32-bit int. Use Move(int) instead.")] @@ -154,47 +179,71 @@ 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); + GC.KeepAlive (this); + return result; } public IntPtr GetMemoryBase () { - return (IntPtr)SkiaApi.sk_stream_get_memory_base (Handle); + var result = (IntPtr)SkiaApi.sk_stream_get_memory_base (Handle); + GC.KeepAlive (this); + return result; } public SKData GetData () { - return SKData.GetObject (SkiaApi.sk_stream_get_data (Handle)); + var result = SKData.GetObject (SkiaApi.sk_stream_get_data (Handle)); + GC.KeepAlive (this); + return result; } - internal SKStream Fork () => GetObject (SkiaApi.sk_stream_fork (Handle)); + internal SKStream Fork () + { + var result = GetObject (SkiaApi.sk_stream_fork (Handle)); + GC.KeepAlive (this); + return result; + } - internal SKStream Duplicate () => GetObject (SkiaApi.sk_stream_duplicate (Handle)); + internal SKStream Duplicate () + { + var result = GetObject (SkiaApi.sk_stream_duplicate (Handle)); + GC.KeepAlive (this); + return result; + } public bool HasPosition { get { - return SkiaApi.sk_stream_has_position (Handle); + var result = SkiaApi.sk_stream_has_position (Handle); + GC.KeepAlive (this); + return result; } } public int Position { get { - return (int)SkiaApi.sk_stream_get_position (Handle); + var result = (int)SkiaApi.sk_stream_get_position (Handle); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } } public int Length { get { - return (int)SkiaApi.sk_stream_get_length (Handle); + var result = (int)SkiaApi.sk_stream_get_length (Handle); + GC.KeepAlive (this); + return result; } } @@ -294,7 +343,13 @@ 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); + GC.KeepAlive (this); + return result; + } + } public static bool IsPathSupported (string path) => true; @@ -363,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); } } @@ -387,80 +444,109 @@ 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } } public bool NewLine () { - return SkiaApi.sk_wstream_newline (Handle); + 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) { - return SkiaApi.sk_wstream_write_8 (Handle, value); + var result = SkiaApi.sk_wstream_write_8 (Handle, value); + GC.KeepAlive (this); + return result; } public bool Write16 (UInt16 value) { - return SkiaApi.sk_wstream_write_16 (Handle, value); + var result = SkiaApi.sk_wstream_write_16 (Handle, value); + GC.KeepAlive (this); + return result; } public bool Write32 (UInt32 value) { - return SkiaApi.sk_wstream_write_32 (Handle, value); + var result = SkiaApi.sk_wstream_write_32 (Handle, value); + GC.KeepAlive (this); + return result; } public bool WriteText (string value) { - return SkiaApi.sk_wstream_write_text (Handle, value); + var result = SkiaApi.sk_wstream_write_text (Handle, value); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } public bool WriteBool (bool value) { - return SkiaApi.sk_wstream_write_bool (Handle, value); + var result = SkiaApi.sk_wstream_write_bool (Handle, value); + GC.KeepAlive (this); + return result; } public bool WriteScalar (float value) { - return SkiaApi.sk_wstream_write_scalar (Handle, value); + var result = SkiaApi.sk_wstream_write_scalar (Handle, value); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return result; } public bool WriteStream (SKStream input, int length) @@ -469,7 +555,10 @@ 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); + GC.KeepAlive (this); + return result; } public static int GetSizeOfPackedUInt32 (UInt32 value) @@ -507,7 +596,13 @@ 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); + GC.KeepAlive (this); + return result; + } + } public static bool IsPathSupported (string path) => true; @@ -546,17 +641,22 @@ 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)); + GC.KeepAlive (this); + 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)); + 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) @@ -567,6 +667,7 @@ public void CopyTo (Span data) fixed (void* d = data) { SkiaApi.sk_dynamicmemorywstream_copy_to (Handle, d); + GC.KeepAlive (this); } } @@ -574,7 +675,10 @@ 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); + GC.KeepAlive (this); + return result; } public bool CopyTo (Stream dst) diff --git a/binding/SkiaSharp/SKString.cs b/binding/SkiaSharp/SKString.cs index b48a84f23f0..5b080c851a5 100644 --- a/binding/SkiaSharp/SKString.cs +++ b/binding/SkiaSharp/SKString.cs @@ -49,7 +49,9 @@ 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); + GC.KeepAlive (this); + return result; } public static explicit operator string (SKString skString) diff --git a/binding/SkiaSharp/SKSurface.cs b/binding/SkiaSharp/SKSurface.cs index ba7fc03f796..d8d8414e872 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,43 @@ 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); + GC.KeepAlive (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); + GC.KeepAlive (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); + GC.KeepAlive (this); + 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)); + GC.KeepAlive (this); + 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)); + GC.KeepAlive (this); + return result; + } public void Draw (SKCanvas canvas, float x, float y, SKPaint paint) { @@ -283,6 +332,9 @@ 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); } public void Draw (SKCanvas canvas, SKPoint p, SKSamplingOptions sampling, SKPaint paint = null) @@ -296,6 +348,9 @@ 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); } public SKPixmap PeekPixels () @@ -316,6 +371,8 @@ 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; return result; diff --git a/binding/SkiaSharp/SKTextBlob.cs b/binding/SkiaSharp/SKTextBlob.cs index 10193d25a0c..97dfffe48c3 100644 --- a/binding/SkiaSharp/SKTextBlob.cs +++ b/binding/SkiaSharp/SKTextBlob.cs @@ -12,19 +12,34 @@ 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); + GC.KeepAlive (this); + } - void ISKNonVirtualReferenceCounted.UnreferenceNative () => SkiaApi.sk_textblob_unref (Handle); + 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; } } - public uint UniqueId => SkiaApi.sk_textblob_get_unique_id (Handle); + public uint UniqueId { + get { + var r = SkiaApi.sk_textblob_get_unique_id (Handle); + GC.KeepAlive (this); + return r; + } + } // Create @@ -240,6 +255,8 @@ 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); + GC.KeepAlive (this); } } @@ -250,7 +267,10 @@ 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); + GC.KeepAlive (this); + return result; } // @@ -274,8 +294,11 @@ 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); + GC.KeepAlive (this); + } // Build @@ -396,6 +419,8 @@ 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); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, 0, 0); } @@ -416,6 +441,8 @@ 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); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, 0, textByteCount); } @@ -438,6 +465,8 @@ 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); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, count, 0); } @@ -458,6 +487,8 @@ 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); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, count, textByteCount); } @@ -481,6 +512,8 @@ public SKRawRunBuffer AllocateRawPositionedRun (SKFont font, int count, else 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); } @@ -501,6 +534,8 @@ 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); + GC.KeepAlive (this); return new SKRawRunBuffer (runbuffer, count, count, textByteCount); } @@ -523,6 +558,8 @@ public SKRawRunBuffer AllocateRawRotationScaleRun (SKFont else 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); } @@ -543,6 +580,8 @@ public SKRawRunBuffer AllocateRawRotationScaleTextRun (SK else 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 46dfff81fcd..73aa2d9361d 100644 --- a/binding/SkiaSharp/SKTypeface.cs +++ b/binding/SkiaSharp/SKTypeface.cs @@ -3,41 +3,21 @@ 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 bool emptyInitialized; + private static object emptyLock = new object (); - 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); - } + private static SKTypeface defaultTypeface; + private static bool defaultTypefaceInitialized; + private static object defaultTypefaceLock = new object (); - internal static void EnsureStaticInstanceAreInitialized () - { - // IMPORTANT: do not remove to ensure that the static instances - // are initialized before any access is made to them - } + private SKFont font; internal SKTypeface (IntPtr handle, bool owns) : base (handle, owns) @@ -49,9 +29,25 @@ internal SKTypeface (IntPtr handle, bool owns) protected override void Dispose (bool disposing) => base.Dispose (disposing); - public static SKTypeface Default => defaultTypeface; - - public static SKTypeface Empty => empty; + 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, + // 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; @@ -60,7 +56,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); } @@ -100,31 +96,91 @@ 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)); + GC.KeepAlive (this); + 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)); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + 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); + GC.KeepAlive (this); + return r; + } + } - public int GlyphCount => SkiaApi.sk_typeface_count_glyphs (Handle); + public int GlyphCount { + get { + var r = SkiaApi.sk_typeface_count_glyphs (Handle); + GC.KeepAlive (this); + 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)); + GC.KeepAlive (this); + return r; + } + } // GetTableTags - public int TableCount => SkiaApi.sk_typeface_count_tables (Handle); + public int TableCount { + get { + var r = SkiaApi.sk_typeface_count_tables (Handle); + GC.KeepAlive (this); + return r; + } + } public UInt32[] GetTableTags () { @@ -139,9 +195,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; @@ -149,8 +207,12 @@ 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); + GC.KeepAlive (this); + return r; + } // GetTableData @@ -179,6 +241,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; } @@ -273,7 +336,9 @@ 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)); + GC.KeepAlive (this); + return r; } } @@ -283,8 +348,13 @@ 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); + GC.KeepAlive (this); + return r; + } + } /// /// Gets a kerning adjustment for each sequential pair of glyph indices in . @@ -329,6 +399,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) @@ -342,8 +413,13 @@ 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); + GC.KeepAlive (this); + return r; + } + } public SKFontVariationAxis[] VariationDesignParameters { @@ -355,6 +431,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; } @@ -367,22 +444,30 @@ 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) { + GC.KeepAlive (this); return total; + } // Skia is all-or-nothing: if buffer is undersized it writes nothing. // Retry with a pooled buffer and copy what fits. 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; } } - 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); + GC.KeepAlive (this); + return r; + } + } public SKFontVariationPositionCoordinate[] VariationDesignPosition { @@ -394,6 +479,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; } @@ -406,14 +492,17 @@ 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) { + GC.KeepAlive (this); return total; + } // Skia is all-or-nothing: if buffer is undersized it writes nothing. // Retry with a pooled buffer and copy what fits. 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; @@ -423,7 +512,9 @@ 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)); + GC.KeepAlive (this); + return r; } } @@ -431,14 +522,18 @@ 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)); + GC.KeepAlive (this); + 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)); + GC.KeepAlive (this); + return r; } } @@ -447,16 +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, bool owns = true, bool unrefExisting = true) => + GetOrAddDisposeProtectedObject (handle, owns, unrefExisting, (h, o) => new SKTypeface (h, o)); - private sealed class SKTypefaceStatic : SKTypeface - { - internal SKTypefaceStatic (IntPtr x) - : base (x, false) - { - } - - protected override void Dispose (bool disposing) { } - } } } 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 { "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 + + + + + + + + + + + + + + + + + diff --git a/tests/Tests/GarbageCleanupFixture.cs b/tests/Tests/GarbageCleanupFixture.cs index d8f17fc9930..33c139afe5c 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)) + // Dispose-protected 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/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/Pr4080UseAfterFreeTest.cs b/tests/Tests/SkiaSharp/Pr4080UseAfterFreeTest.cs new file mode 100644 index 00000000000..63c65cb551b --- /dev/null +++ b/tests/Tests/SkiaSharp/Pr4080UseAfterFreeTest.cs @@ -0,0 +1,84 @@ +#nullable disable +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using Xunit; + +namespace SkiaSharp.Tests +{ + // Regression guard for the issue #3817 / PR #4080 native use-after-free. + // + // A native wrapper passed to a native call must stay alive for the duration of that + // call. `SKPathMeasure(path)` reads `path.Handle` and then calls + // sk_pathmeasure_new_with_path(...). If nothing keeps `path` rooted across the + // P/Invoke, a concurrent GC can collect + FINALIZE it mid-call, freeing the native + // SkPath while the native ctor is still reading it (SkPath::isFinite) -> 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 () + { + 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 + // 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; + } + } +} 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/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..5fa826c6ed9 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 { @@ -389,45 +391,6 @@ public void CicpPqPngImageHasPqTransferFunction() Assert.False(info.ColorSpace.IsSrgb); } - [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 - var colorspace1 = SKColorSpace.CreateSrgbLinear(); - Assert.Equal("SkiaSharp.SKColorSpace+SKColorSpaceStatic", colorspace1.GetType().FullName); - Assert.Equal(NativeInstanceCount, 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.Same(colorspace1, colorspace2); - Assert.Equal(NativeInstanceCount, colorspace1.GetReferenceCount()); - Assert.Equal(NativeInstanceCount, 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()); - - colorspace3.Dispose(); - Assert.True(colorspace3.IsDisposed); - Assert.Equal(NativeInstanceCount, colorspace1.GetReferenceCount()); - - colorspace2.Dispose(); - Assert.False(colorspace2.IsDisposed); - Assert.Equal(NativeInstanceCount, colorspace1.GetReferenceCount()); - - colorspace1.Dispose(); - Assert.False(colorspace1.IsDisposed); - Assert.Equal(NativeInstanceCount, 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 new file mode 100644 index 00000000000..5e94373e271 --- /dev/null +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryDisposeProtectedTest.cs @@ -0,0 +1,102 @@ +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. + [Collection (HandleDictionaryThreadingCollection.Name)] + 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 () + { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + + 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/SKHandleDictionaryReentrancyTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs new file mode 100644 index 00000000000..85407d50128 --- /dev/null +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryReentrancyTest.cs @@ -0,0 +1,63 @@ +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) + [Collection (HandleDictionaryThreadingCollection.Name)] + 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 _)); + } + } +} diff --git a/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs b/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs new file mode 100644 index 00000000000..eb4cbc821f8 --- /dev/null +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryReservationTest.cs @@ -0,0 +1,193 @@ +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. + [Collection (HandleDictionaryThreadingCollection.Name)] + public class SKHandleDictionaryReservationTest : SKTest + { + // --- 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; + + 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 () + { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + + 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]); + // 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 { + 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 (); + } + } + + // --- 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; + + 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..05b2694c3ee --- /dev/null +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryStaleWrapperTest.cs @@ -0,0 +1,258 @@ +using System; +using System.Threading; +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. + [Collection (HandleDictionaryThreadingCollection.Name)] + 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 () + { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + + 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 = RunOnThread (() => 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 = 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; + + // 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 () + { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + + 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 = RunOnThread (() => 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 = RunOnThread (() => 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 () + { + SkipOnPlatform (IsBrowser, "WASM is single-threaded; this test requires real OS threads"); + + 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; + ThreadResult disposer1 = null, disposer2 = null; + + try { + // Park W1 (disposed) in its cleanup window. + 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 = 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 = 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 = 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); + 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..10ac97feee5 --- /dev/null +++ b/tests/Tests/SkiaSharp/SKHandleDictionaryTestHelpers.cs @@ -0,0 +1,268 @@ +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 + { + // 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 () => + 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 sw = System.Diagnostics.Stopwatch.StartNew (); + var allJoined = true; + foreach (var t in threads) { + var remaining = (int) Math.Max (0, timeoutMs - sw.ElapsedMilliseconds); + 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.Capture (captured).Throw (); + } + + // 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. + // + // 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)) + 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 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 + // 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/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 94071469874..858450a7a3e 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,260 @@ 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); + } + + // 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/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..16dd73b1c39 --- /dev/null +++ b/tests/Tests/SkiaSharp/SKSingletonConcurrencyTest.cs @@ -0,0 +1,164 @@ +using System; +using Xunit; + +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). + // + // 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 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. + // + // 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; + + 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]; + + SKHandleDictionaryTestHelpers.RunConcurrent(threadCount, i => + { + 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)."); + + 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)); + } + } +} diff --git a/tests/Tests/SkiaSharp/SKSingletonInitTest.cs b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs new file mode 100644 index 00000000000..6541076143a --- /dev/null +++ b/tests/Tests/SkiaSharp/SKSingletonInitTest.cs @@ -0,0 +1,254 @@ +using System; +using System.Reflection; +using System.Runtime.ExceptionServices; +#if !NETFRAMEWORK +using System.Runtime.Loader; +#endif +using Xunit; + +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 --- + + [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 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() + { + 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()); + } + + // --- 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); + } + +#if !NETFRAMEWORK + // --- #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() + { + 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 + { + 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) + { + // 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); + } + 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; + } + } +#endif + } +} 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); }