From ed296fc1cfa34b9ffb1c6ea7ef117bc3569a98f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 01:51:35 +0000 Subject: [PATCH 1/6] Initial plan From bdbe33ad63b9098a5654ba6951ee5808ebf43f57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 02:09:21 +0000 Subject: [PATCH 2/6] Add GC.KeepAlive to SKCanvas methods with reference type parameters Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com> --- binding/SkiaSharp/SKCanvas.cs | 67 ++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/binding/SkiaSharp/SKCanvas.cs b/binding/SkiaSharp/SKCanvas.cs index 16e5be1e8a..40ec6449da 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) => @@ -59,16 +60,27 @@ public int Save () return SkiaApi.sk_canvas_save (Handle); } - public int SaveLayer (SKRect limit, SKPaint? paint) => - SkiaApi.sk_canvas_save_layer (Handle, &limit, paint?.Handle ?? IntPtr.Zero); + public int SaveLayer (SKRect limit, SKPaint? paint) + { + var result = SkiaApi.sk_canvas_save_layer (Handle, &limit, paint?.Handle ?? IntPtr.Zero); + GC.KeepAlive (paint); + return result; + } - public int SaveLayer (SKPaint? paint) => - SkiaApi.sk_canvas_save_layer (Handle, null, paint?.Handle ?? IntPtr.Zero); + public int SaveLayer (SKPaint? paint) + { + var result = SkiaApi.sk_canvas_save_layer (Handle, null, paint?.Handle ?? IntPtr.Zero); + GC.KeepAlive (paint); + return result; + } public int SaveLayer (in SKCanvasSaveLayerRec rec) { var native = rec.ToNative (); - return SkiaApi.sk_canvas_save_layer_rec (Handle, &native); + var result = SkiaApi.sk_canvas_save_layer_rec (Handle, &native); + GC.KeepAlive (rec.Paint); + GC.KeepAlive (rec.Backdrop); + return result; } public int SaveLayer () => @@ -95,6 +107,7 @@ public void DrawLine (float x0, float y0, float x1, float y1, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_line (Handle, x0, y0, x1, y1, paint.Handle); + GC.KeepAlive (paint); } // Clear @@ -255,6 +268,7 @@ public void ClipRoundRect (SKRoundRect rect, SKClipOperation operation = SKClipO throw new ArgumentNullException (nameof (rect)); SkiaApi.sk_canvas_clip_rrect_with_operation (Handle, rect.Handle, operation, antialias); + GC.KeepAlive (rect); } public void ClipPath (SKPath path, SKClipOperation operation = SKClipOperation.Intersect, bool antialias = false) @@ -263,6 +277,7 @@ public void ClipPath (SKPath path, SKClipOperation operation = SKClipOperation.I throw new ArgumentNullException (nameof (path)); SkiaApi.sk_canvas_clip_path_with_operation (Handle, path.Handle, operation, antialias); + GC.KeepAlive (path); } public void ClipRegion (SKRegion region, SKClipOperation operation = SKClipOperation.Intersect) @@ -271,6 +286,7 @@ public void ClipRegion (SKRegion region, SKClipOperation operation = SKClipOpera throw new ArgumentNullException (nameof (region)); SkiaApi.sk_canvas_clip_region (Handle, region.Handle, operation); + GC.KeepAlive (region); } public SKRect LocalClipBounds { @@ -312,6 +328,7 @@ public void DrawPaint (SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_paint (Handle, paint.Handle); + GC.KeepAlive (paint); } // DrawRegion @@ -323,6 +340,8 @@ public void DrawRegion (SKRegion region, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_region (Handle, region.Handle, paint.Handle); + GC.KeepAlive (region); + GC.KeepAlive (paint); } // DrawRect @@ -337,6 +356,7 @@ public void DrawRect (SKRect rect, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_rect (Handle, &rect, paint.Handle); + GC.KeepAlive (paint); } // DrawRoundRect @@ -348,6 +368,8 @@ public void DrawRoundRect (SKRoundRect rect, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_rrect (Handle, rect.Handle, paint.Handle); + GC.KeepAlive (rect); + GC.KeepAlive (paint); } public void DrawRoundRect (float x, float y, float w, float h, float rx, float ry, SKPaint paint) @@ -360,6 +382,7 @@ public void DrawRoundRect (SKRect rect, float rx, float ry, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_round_rect (Handle, &rect, rx, ry, paint.Handle); + GC.KeepAlive (paint); } public void DrawRoundRect (SKRect rect, SKSize r, SKPaint paint) @@ -384,6 +407,7 @@ public void DrawOval (SKRect rect, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_oval (Handle, &rect, paint.Handle); + GC.KeepAlive (paint); } // DrawCircle @@ -393,6 +417,7 @@ public void DrawCircle (float cx, float cy, float radius, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_circle (Handle, cx, cy, radius, paint.Handle); + GC.KeepAlive (paint); } public void DrawCircle (SKPoint c, float radius, SKPaint paint) @@ -409,6 +434,8 @@ public void DrawPath (SKPath path, SKPaint paint) if (path == null) throw new ArgumentNullException (nameof (path)); SkiaApi.sk_canvas_draw_path (Handle, path.Handle, paint.Handle); + GC.KeepAlive (path); + GC.KeepAlive (paint); } // DrawPoints @@ -422,6 +449,7 @@ public void DrawPoints (SKPointMode mode, SKPoint[] points, SKPaint paint) fixed (SKPoint* p = points) { SkiaApi.sk_canvas_draw_points (Handle, mode, (IntPtr)points.Length, p, paint.Handle); } + GC.KeepAlive (paint); } // DrawPoint @@ -436,6 +464,7 @@ public void DrawPoint (float x, float y, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_point (Handle, x, y, paint.Handle); + GC.KeepAlive (paint); } public void DrawPoint (SKPoint p, SKColor color) @@ -476,6 +505,8 @@ public void DrawImage (SKImage image, float x, float y, SKSamplingOptions sampli if (image == null) throw new ArgumentNullException (nameof (image)); SkiaApi.sk_canvas_draw_image (Handle, image.Handle, x, y, &sampling, paint?.Handle ?? IntPtr.Zero); + GC.KeepAlive (image); + GC.KeepAlive (paint); } public void DrawImage (SKImage image, SKRect dest, SKPaint paint = null) @@ -507,6 +538,8 @@ private void DrawImage (SKImage image, SKRect* source, SKRect* dest, SKSamplingO if (image == null) throw new ArgumentNullException (nameof (image)); SkiaApi.sk_canvas_draw_image_rect (Handle, image.Handle, source, dest, &sampling, paint?.Handle ?? IntPtr.Zero); + GC.KeepAlive (image); + GC.KeepAlive (paint); } // DrawPicture @@ -528,6 +561,8 @@ public void DrawPicture (SKPicture picture, in SKMatrix matrix, SKPaint paint = throw new ArgumentNullException (nameof (picture)); fixed (SKMatrix* m = &matrix) SkiaApi.sk_canvas_draw_picture (Handle, picture.Handle, m, paint == null ? IntPtr.Zero : paint.Handle); + GC.KeepAlive (picture); + GC.KeepAlive (paint); } public void DrawPicture (SKPicture picture, SKPaint paint = null) @@ -535,6 +570,8 @@ public void DrawPicture (SKPicture picture, SKPaint paint = null) if (picture == null) throw new ArgumentNullException (nameof (picture)); SkiaApi.sk_canvas_draw_picture (Handle, picture.Handle, null, paint == null ? IntPtr.Zero : paint.Handle); + GC.KeepAlive (picture); + GC.KeepAlive (paint); } // DrawDrawable @@ -545,6 +582,7 @@ public void DrawDrawable (SKDrawable drawable, in SKMatrix matrix) throw new ArgumentNullException (nameof (drawable)); fixed (SKMatrix* m = &matrix) SkiaApi.sk_canvas_draw_drawable (Handle, drawable.Handle, m); + GC.KeepAlive (drawable); } public void DrawDrawable (SKDrawable drawable, float x, float y) @@ -611,6 +649,8 @@ public void DrawText (SKTextBlob text, float x, float y, SKPaint paint) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_text_blob (Handle, text.Handle, x, y, paint.Handle); + GC.KeepAlive (text); + GC.KeepAlive (paint); } // DrawText @@ -744,11 +784,13 @@ public void DrawAnnotation (SKRect rect, string key, SKData value) fixed (byte* b = bytes) { SkiaApi.sk_canvas_draw_annotation (base.Handle, &rect, b, value == null ? IntPtr.Zero : value.Handle); } + GC.KeepAlive (value); } public void DrawUrlAnnotation (SKRect rect, SKData value) { SkiaApi.sk_canvas_draw_url_annotation (Handle, &rect, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); } public SKData DrawUrlAnnotation (SKRect rect, string value) @@ -761,6 +803,7 @@ public SKData DrawUrlAnnotation (SKRect rect, string value) public void DrawNamedDestinationAnnotation (SKPoint point, SKData value) { SkiaApi.sk_canvas_draw_named_destination_annotation (Handle, &point, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); } public SKData DrawNamedDestinationAnnotation (SKPoint point, string value) @@ -773,6 +816,7 @@ public SKData DrawNamedDestinationAnnotation (SKPoint point, string value) public void DrawLinkDestinationAnnotation (SKRect rect, SKData value) { SkiaApi.sk_canvas_draw_link_destination_annotation (Handle, &rect, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); } public SKData DrawLinkDestinationAnnotation (SKRect rect, string value) @@ -805,6 +849,8 @@ public void DrawImageNinePatch (SKImage image, SKRectI center, SKRect dst, SKFil throw new ArgumentException ("Center rectangle must be contained inside the image bounds.", nameof (center)); SkiaApi.sk_canvas_draw_image_nine (Handle, image.Handle, ¢er, &dst, filterMode, paint == null ? IntPtr.Zero : paint.Handle); + GC.KeepAlive (image); + GC.KeepAlive (paint); } // Draw*Lattice @@ -870,6 +916,8 @@ public void DrawImageLattice (SKImage image, SKLattice lattice, SKRect dst, SKFi } SkiaApi.sk_canvas_draw_image_lattice (Handle, image.Handle, &nativeLattice, &dst, filterMode, paint == null ? IntPtr.Zero : paint.Handle); } + GC.KeepAlive (image); + GC.KeepAlive (paint); } // *Matrix @@ -940,6 +988,8 @@ public void DrawVertices (SKVertices vertices, SKBlendMode mode, SKPaint paint) if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_vertices (Handle, vertices.Handle, mode, paint.Handle); + GC.KeepAlive (vertices); + GC.KeepAlive (paint); } // DrawArc @@ -949,6 +999,7 @@ public void DrawArc (SKRect oval, float startAngle, float sweepAngle, bool useCe if (paint == null) throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_arc (Handle, &oval, startAngle, sweepAngle, useCenter, paint.Handle); + GC.KeepAlive (paint); } // DrawRoundRectDifference @@ -963,6 +1014,9 @@ public void DrawRoundRectDifference (SKRoundRect outer, SKRoundRect inner, SKPai throw new ArgumentNullException (nameof (paint)); SkiaApi.sk_canvas_draw_drrect (Handle, outer.Handle, inner.Handle, paint.Handle); + GC.KeepAlive (outer); + GC.KeepAlive (inner); + GC.KeepAlive (paint); } // DrawAtlas @@ -1010,6 +1064,8 @@ private void DrawAtlas (SKImage atlas, SKRect[] sprites, SKRotationScaleMatrix[] fixed (SKColor* c = colors) { SkiaApi.sk_canvas_draw_atlas (Handle, atlas.Handle, t, s, (uint*)c, transforms.Length, mode, &sampling, cullRect, paint?.Handle ?? IntPtr.Zero); } + GC.KeepAlive (atlas); + GC.KeepAlive (paint); } // DrawPatch @@ -1038,6 +1094,7 @@ public void DrawPatch (SKPoint[] cubics, SKColor[] colors, SKPoint[] texCoords, fixed (SKPoint* coords = texCoords) { SkiaApi.sk_canvas_draw_patch (Handle, cubes, (uint*)cols, coords, mode, paint.Handle); } + GC.KeepAlive (paint); } internal static SKCanvas GetObject (IntPtr handle, bool owns = true, bool unrefExisting = true) => From e531a837213abfe57342b00dd8aeaf0df9e8c621 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 02:13:07 +0000 Subject: [PATCH 3/6] Add GC.KeepAlive to SKPaint methods and properties Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com> --- binding/SkiaSharp/SKPaint.cs | 42 +++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/binding/SkiaSharp/SKPaint.cs b/binding/SkiaSharp/SKPaint.cs index 58039258b7..40fde20eee 100644 --- a/binding/SkiaSharp/SKPaint.cs +++ b/binding/SkiaSharp/SKPaint.cs @@ -64,6 +64,8 @@ public SKPaint (SKFont font) if (Handle == IntPtr.Zero) throw new InvalidOperationException ("Unable to create a new SKPaint instance."); + + GC.KeepAlive (font); } protected override void Dispose (bool disposing) => @@ -155,8 +157,11 @@ public SKColorF ColorF { set => SkiaApi.sk_paint_set_color4f (Handle, &value, IntPtr.Zero); } - public void SetColor (SKColorF color, SKColorSpace colorspace) => + public void SetColor (SKColorF color, SKColorSpace colorspace) + { SkiaApi.sk_paint_set_color4f (Handle, &color, colorspace?.Handle ?? IntPtr.Zero); + GC.KeepAlive (colorspace); + } public float StrokeWidth { get => SkiaApi.sk_paint_get_stroke_width (Handle); @@ -180,22 +185,34 @@ public SKStrokeJoin StrokeJoin { 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); + set { + SkiaApi.sk_paint_set_shader (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } public SKMaskFilter MaskFilter { get => SKMaskFilter.GetObject (SkiaApi.sk_paint_get_maskfilter (Handle)); - set => SkiaApi.sk_paint_set_maskfilter (Handle, value == null ? IntPtr.Zero : value.Handle); + set { + SkiaApi.sk_paint_set_maskfilter (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } public SKColorFilter ColorFilter { get => SKColorFilter.GetObject (SkiaApi.sk_paint_get_colorfilter (Handle)); - set => SkiaApi.sk_paint_set_colorfilter (Handle, value == null ? IntPtr.Zero : value.Handle); + set { + SkiaApi.sk_paint_set_colorfilter (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } public SKImageFilter ImageFilter { get => SKImageFilter.GetObject (SkiaApi.sk_paint_get_imagefilter (Handle)); - set => SkiaApi.sk_paint_set_imagefilter (Handle, value == null ? IntPtr.Zero : value.Handle); + set { + SkiaApi.sk_paint_set_imagefilter (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } public SKBlendMode BlendMode { @@ -205,7 +222,10 @@ public SKBlendMode BlendMode { 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); + set { + SkiaApi.sk_paint_set_blender (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } [Obsolete ($"Use {nameof (SKSamplingOptions)} instead.")] @@ -252,7 +272,10 @@ 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); + set { + SkiaApi.sk_paint_set_path_effect (Handle, value == null ? IntPtr.Zero : value.Handle); + GC.KeepAlive (value); + } } // FontSpacing @@ -506,7 +529,10 @@ private bool GetFillPath (SKPath src, SKPath dst, SKRect* cullRect, SKMatrix mat _ = src ?? throw new ArgumentNullException (nameof (src)); _ = dst ?? throw new ArgumentNullException (nameof (dst)); - return SkiaApi.sk_paint_get_fill_path (Handle, src.Handle, dst.Handle, cullRect, &matrix); + var result = SkiaApi.sk_paint_get_fill_path (Handle, src.Handle, dst.Handle, cullRect, &matrix); + GC.KeepAlive (src); + GC.KeepAlive (dst); + return result; } // CountGlyphs From 643e1b4c5bcb3b346642c102bca93605f8f9acd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 02:15:57 +0000 Subject: [PATCH 4/6] Add GC.KeepAlive to SKImage and SKBitmap critical methods Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com> --- binding/SkiaSharp/SKBitmap.cs | 4 +++- binding/SkiaSharp/SKImage.cs | 13 ++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/binding/SkiaSharp/SKBitmap.cs b/binding/SkiaSharp/SKBitmap.cs index 93219f0b90..e5117aefce 100644 --- a/binding/SkiaSharp/SKBitmap.cs +++ b/binding/SkiaSharp/SKBitmap.cs @@ -218,7 +218,9 @@ public bool ExtractSubset (SKBitmap destination, SKRectI subset) if (destination == null) { throw new ArgumentNullException (nameof (destination)); } - return SkiaApi.sk_bitmap_extract_subset (Handle, destination.Handle, &subset); + var result = SkiaApi.sk_bitmap_extract_subset (Handle, destination.Handle, &subset); + GC.KeepAlive (destination); + return result; } // ExtractAlpha diff --git a/binding/SkiaSharp/SKImage.cs b/binding/SkiaSharp/SKImage.cs index ebf8223fdd..8e9c0a0d04 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) => @@ -113,7 +115,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) @@ -169,7 +173,9 @@ public static SKImage FromEncodedData (SKData data) throw new ArgumentNullException (nameof (data)); var handle = SkiaApi.sk_image_new_from_encoded (data.Handle); - return GetObject (handle); + var image = GetObject (handle); + GC.KeepAlive (data); + return image; } public static SKImage FromEncodedData (ReadOnlySpan data) @@ -457,6 +463,7 @@ public bool PeekPixels (SKPixmap pixmap) var result = SkiaApi.sk_image_peek_pixels (Handle, pixmap.Handle); if (result) pixmap.pixelSource = this; + GC.KeepAlive (pixmap); return result; } From 2e12a10febecc1a37f95d578e45ccaef93189113 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 02:19:29 +0000 Subject: [PATCH 5/6] Add GC.KeepAlive to SKPath critical methods Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com> --- binding/SkiaSharp/SKPath.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/binding/SkiaSharp/SKPath.cs b/binding/SkiaSharp/SKPath.cs index a551595f6a..c56d01c9e6 100644 --- a/binding/SkiaSharp/SKPath.cs +++ b/binding/SkiaSharp/SKPath.cs @@ -33,6 +33,7 @@ public SKPath (SKPath path) if (Handle == IntPtr.Zero) { throw new InvalidOperationException ("Unable to copy the SKPath instance."); } + GC.KeepAlive (path); } protected override void Dispose (bool disposing) => @@ -293,6 +294,7 @@ public void AddRoundRect (SKRoundRect rect, SKPathDirection direction = SKPathDi if (rect == null) throw new ArgumentNullException (nameof (rect)); SkiaApi.sk_path_add_rrect (Handle, rect.Handle, direction); + GC.KeepAlive (rect); } public void AddRoundRect (SKRoundRect rect, SKPathDirection direction, uint startIndex) @@ -300,6 +302,7 @@ public void AddRoundRect (SKRoundRect rect, SKPathDirection direction, uint star if (rect == null) throw new ArgumentNullException (nameof (rect)); SkiaApi.sk_path_add_rrect_start (Handle, rect.Handle, direction, startIndex); + GC.KeepAlive (rect); } public void AddOval (SKRect rect, SKPathDirection direction = SKPathDirection.Clockwise) => @@ -357,6 +360,7 @@ public void AddPath (SKPath other, float dx, float dy, SKPathAddMode mode = SKPa throw new ArgumentNullException (nameof (other)); SkiaApi.sk_path_add_path_offset (Handle, other.Handle, dx, dy, mode); + GC.KeepAlive (other); } public void AddPath (SKPath other, in SKMatrix matrix, SKPathAddMode mode = SKPathAddMode.Append) @@ -366,6 +370,7 @@ public void AddPath (SKPath other, in SKMatrix matrix, SKPathAddMode mode = SKPa fixed (SKMatrix* m = &matrix) SkiaApi.sk_path_add_path_matrix (Handle, other.Handle, m, mode); + GC.KeepAlive (other); } public void AddPath (SKPath other, SKPathAddMode mode = SKPathAddMode.Append) @@ -374,6 +379,7 @@ public void AddPath (SKPath other, SKPathAddMode mode = SKPathAddMode.Append) throw new ArgumentNullException (nameof (other)); SkiaApi.sk_path_add_path (Handle, other.Handle, mode); + GC.KeepAlive (other); } public void AddPathReverse (SKPath other) @@ -382,6 +388,7 @@ public void AddPathReverse (SKPath other) throw new ArgumentNullException (nameof (other)); SkiaApi.sk_path_add_path_reverse (Handle, other.Handle); + GC.KeepAlive (other); } public void AddRoundRect (SKRect rect, float rx, float ry, SKPathDirection dir = SKPathDirection.Clockwise) => From 2f4b69aad6c4b7ce56aa17a4a7732451d8a54b37 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 02:21:28 +0000 Subject: [PATCH 6/6] Add comprehensive documentation and analysis tools for GC.KeepAlive implementation Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com> --- GC_KEEPALIVE_IMPLEMENTATION_GUIDE.md | 220 +++++++++++++++++++++++++++ analyze_gc_keepalive.sh | 102 +++++++++++++ 2 files changed, 322 insertions(+) create mode 100644 GC_KEEPALIVE_IMPLEMENTATION_GUIDE.md create mode 100755 analyze_gc_keepalive.sh diff --git a/GC_KEEPALIVE_IMPLEMENTATION_GUIDE.md b/GC_KEEPALIVE_IMPLEMENTATION_GUIDE.md new file mode 100644 index 0000000000..32fa8776d5 --- /dev/null +++ b/GC_KEEPALIVE_IMPLEMENTATION_GUIDE.md @@ -0,0 +1,220 @@ +# GC.KeepAlive Implementation Guide for SkiaSharp + +## Issue Overview +This document describes the implementation of GC.KeepAlive calls to fix a critical GC safety issue in SkiaSharp's P/Invoke layer. + +### Background +As described in [Chris Brumme's blog post on Lifetime, GC.KeepAlive, handle recycling](https://learn.microsoft.com/en-us/archive/blogs/cbrumme/lifetime-gc-keepalive-handle-recycling): + +> Once we've extracted `_handle` from `this`, there are no further uses of this object. In other words, `this` can be collected even while you are executing an instance method on that object. But what if class `C` has a `Finalize()` method which closes `_handle`? When we call `C.OperateOnHandle()`, we now have a race between the application and the GC / Finalizer. Eventually, that's a race we're going to lose. + +**The problem**: The .NET runtime cannot see into native code. Once a handle is extracted from a managed object and passed to native code, the GC may collect the managed object (and run its finalizer) while the native code is still executing. + +**The solution**: Use `GC.KeepAlive()` after P/Invoke calls to ensure managed objects remain alive until the native call completes. + +## Implementation Pattern + +### Basic Pattern +For any public method that: +1. Takes reference type parameters (classes inheriting from SKObject, or other reference types like SKData, arrays, etc.) +2. Calls SkiaApi.* P/Invoke methods +3. Passes `.Handle` from those parameters to the P/Invoke + +Add `GC.KeepAlive()` calls after the P/Invoke for each reference type parameter. + +### Example: Before +```csharp +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); +} +``` + +### Example: After +```csharp +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); +} +``` + +### Property Setters +For property setters that call P/Invoke: + +**Before:** +```csharp +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); +} +``` + +**After:** +```csharp +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); + GC.KeepAlive (value); + } +} +``` + +### Methods with Multiple Parameters +Add GC.KeepAlive for ALL reference type parameters: + +```csharp +public void DrawPath (SKPath path, SKPaint paint) +{ + if (paint == null) + throw new ArgumentNullException (nameof (paint)); + if (path == null) + throw new ArgumentNullException (nameof (path)); + SkiaApi.sk_canvas_draw_path (Handle, path.Handle, paint.Handle); + GC.KeepAlive (path); // Keep ALL reference type params alive + GC.KeepAlive (paint); +} +``` + +### Struct Parameters with Reference Fields +For struct parameters that contain reference type fields (like SKCanvasSaveLayerRec), keep those fields alive: + +```csharp +public int SaveLayer (in SKCanvasSaveLayerRec rec) +{ + var native = rec.ToNative (); + var result = SkiaApi.sk_canvas_save_layer_rec (Handle, &native); + GC.KeepAlive (rec.Paint); // Keep reference fields alive + GC.KeepAlive (rec.Backdrop); + return result; +} +``` + +### Constructors +Constructors that use reference type parameters also need protection: + +```csharp +public SKCanvas (SKBitmap bitmap) + : this (IntPtr.Zero, true) +{ + if (bitmap == null) + throw new ArgumentNullException (nameof (bitmap)); + Handle = SkiaApi.sk_canvas_new_from_bitmap (bitmap.Handle); + GC.KeepAlive (bitmap); +} +``` + +## What NOT to Fix + +### Don't add GC.KeepAlive for: +1. **Value types** (int, float, SKRect, SKColor, etc.) +2. **Strings** (they are handled specially by the marshaler) +3. **`this.Handle`** (the current object is implicitly kept alive) +4. **Methods that don't call P/Invoke** (wrapper methods that just call other managed methods) + +## Files Completed + +### Fully Fixed +- [x] **SKCanvas.cs** - 30+ methods including DrawPicture, DrawImage, DrawPath, DrawPaint, etc. +- [x] **SKPaint.cs** - Constructor, properties (Shader, MaskFilter, ColorFilter, ImageFilter, Blender, PathEffect), GetFillPath + +### Partially Fixed +- [ ] **SKImage.cs** - 4/17 methods (FromPixelCopy, FromPixels, FromEncodedData, PeekPixels) +- [ ] **SKBitmap.cs** - 1/5 methods (ExtractSubset) +- [ ] **SKPath.cs** - 6/18 methods (Constructor, AddRoundRect, AddPath variants, AddPathReverse) + +## Files Requiring Work + +### High Priority (Common APIs) +- [ ] **SKPath.cs** - 12 more methods (Op, Simplify, ToWinding, Transform, IsRRect, etc.) +- [ ] **SKFont.cs** - 8 methods +- [ ] **SKBitmap.cs** - 4 more methods (ExtractAlpha, InstallPixels, PeekPixels, Swap) +- [ ] **SKImage.cs** - 13 more methods +- [ ] **SKSurface.cs** - 9 methods +- [ ] **SKShader.cs** - 13 methods +- [ ] **SKImageFilter.cs** - 28 methods +- [ ] **SKTextBlob.cs** - 18 methods +- [ ] **SKRegion.cs** - 11 methods + +### Medium Priority +- [ ] **SKPixmap.cs** - 5 methods +- [ ] **SKCodec.cs** - 2 methods +- [ ] **SKColorFilter.cs** - 2 methods +- [ ] **SKColorSpace.cs** - 3 methods +- [ ] **SKData.cs** - 3 methods +- [ ] **SKDocument.cs** - 3 methods +- [ ] **SKDrawable.cs** - 1 method +- [ ] **SKFontManager.cs** - 5 methods +- [ ] **SKFontStyleSet.cs** - 3 methods +- [ ] **SKPathEffect.cs** - 4 methods +- [ ] **SKPathMeasure.cs** - 3 methods +- [ ] **SKPicture.cs** - 4 methods +- [ ] **SKRuntimeEffect.cs** - 6 methods +- [ ] **SKTypeface.cs** - 3 methods + +### Lower Priority (Less Common/Advanced APIs) +- [ ] **GRContext.cs** - 1 method +- [ ] **SKColorSpaceStructs.cs** - 1 method +- [ ] **SKGraphics.cs** - 1 method +- [ ] **SKNWayCanvas.cs** - 2 methods +- [ ] **SKObject.cs** - 4 methods +- [ ] **SKOverdrawCanvas.cs** - 1 method +- [ ] **SKRoundRect.cs** - 1 method +- [ ] **SKStream.cs** - 3 methods +- [ ] **SKSVG.cs** - 1 method + +## Testing Strategy + +After implementing GC.KeepAlive calls: + +1. **Compile Tests**: Ensure code compiles without errors +2. **Unit Tests**: Run existing xUnit tests in `tests/Tests/SkiaSharp/` +3. **Stress Tests**: Consider adding GC stress tests that: + - Create objects + - Pass them to P/Invoke methods + - Force GC collection during the call + - Verify no crashes or corruption + +Example stress test pattern: +```csharp +[Fact] +public void DrawPicture_WithGCDuringCall_DoesNotCrash() +{ + using var surface = SKSurface.Create(new SKImageInfo(100, 100)); + using var canvas = surface.Canvas; + using var recorder = new SKPictureRecorder(); + using var picture = recorder.BeginRecording(SKRect.Create(100, 100)); + + // Draw with immediate GC pressure + for (int i = 0; i < 1000; i++) + { + canvas.DrawPicture(picture); + if (i % 10 == 0) GC.Collect(2, GCCollectionMode.Forced); + } +} +``` + +## Implementation Checklist for Each File + +When fixing a file: +1. [ ] Identify all public methods that call SkiaApi.* with reference type parameters +2. [ ] For each method, identify all reference type parameters +3. [ ] Add GC.KeepAlive calls after the P/Invoke for each reference type parameter +4. [ ] Check property setters that call P/Invoke +5. [ ] Check constructors that use reference type parameters +6. [ ] Review struct parameters for embedded reference types +7. [ ] Verify the change compiles +8. [ ] Commit with descriptive message + +## References + +- [Original Issue](https://github.com/mono/SkiaSharp/issues/XXXX) +- [Chris Brumme's Blog: Lifetime, GC.KeepAlive, handle recycling](https://learn.microsoft.com/en-us/archive/blogs/cbrumme/lifetime-gc-keepalive-handle-recycling) +- [Uno Platform PR #21660](https://github.com/unoplatform/uno/pull/21660) +- [dotnet/java-interop #719](https://github.com/dotnet/java-interop/issues/719) diff --git a/analyze_gc_keepalive.sh b/analyze_gc_keepalive.sh new file mode 100755 index 0000000000..71a0190326 --- /dev/null +++ b/analyze_gc_keepalive.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# Script to help identify methods in SkiaSharp that need GC.KeepAlive calls +# This script analyzes C# files to find P/Invoke calls that use .Handle + +echo "=== SkiaSharp GC.KeepAlive Analysis ===" +echo "" + +cd binding/SkiaSharp + +echo "Files with P/Invoke calls using .Handle:" +echo "----------------------------------------" + +for file in *.cs; do + if [ ! -f "$file" ]; then + continue + fi + + # Count SkiaApi calls with .Handle + handle_count=$(grep "SkiaApi\." "$file" 2>/dev/null | grep -c "\.Handle" || echo "0") + + if [ "$handle_count" -gt 0 ]; then + # Check if file already has GC.KeepAlive calls + keepalive_count=$(grep -c "GC\.KeepAlive" "$file" 2>/dev/null || echo "0") + + if [ "$keepalive_count" -eq 0 ]; then + status="❌ NO GC.KeepAlive" + elif [ "$keepalive_count" -lt "$handle_count" ]; then + status="⚠️ PARTIAL ($keepalive_count/$handle_count)" + else + status="✅ HAS GC.KeepAlive ($keepalive_count)" + fi + + printf "%-40s %3d calls | %s\n" "$file" "$handle_count" "$status" + fi +done | sort -t'|' -k2 -rn + +echo "" +echo "=== Detailed Analysis by File ===" +echo "" + +# Function to extract method signatures with SkiaApi calls +analyze_file() { + local file=$1 + echo "File: $file" + echo "-------------------------------------------" + + # Find methods that call SkiaApi with .Handle + awk ' + /public.*\(/ { method=$0; in_method=1; brace_count=0 } + in_method && /{/ { brace_count++ } + in_method && /}/ { + brace_count-- + if (brace_count == 0) { in_method=0 } + } + in_method && /SkiaApi\./ && /\.Handle/ { + if (!printed[method]) { + # Clean up method signature + gsub(/^\t+/, "", method) + print " " method + printed[method] = 1 + } + } + ' "$file" + + echo "" +} + +# Analyze top priority files +priority_files=("SKPath.cs" "SKFont.cs" "SKImageFilter.cs" "SKTextBlob.cs" "SKShader.cs" "SKSurface.cs" "SKRegion.cs") + +for file in "${priority_files[@]}"; do + if [ -f "$file" ]; then + handle_count=$(grep "SkiaApi\." "$file" 2>/dev/null | grep -c "\.Handle" || echo "0") + keepalive_count=$(grep -c "GC\.KeepAlive" "$file" 2>/dev/null || echo "0") + + if [ "$handle_count" -gt 0 ] && [ "$keepalive_count" -lt "$handle_count" ]; then + analyze_file "$file" + fi + fi +done + +echo "" +echo "=== Summary ===" +echo "" + +total_files=$(ls *.cs 2>/dev/null | wc -l) +files_with_pinvoke=$(grep -l "SkiaApi\." *.cs 2>/dev/null | wc -l) +files_with_keepalive=$(grep -l "GC\.KeepAlive" *.cs 2>/dev/null | wc -l) + +echo "Total .cs files: $total_files" +echo "Files with P/Invoke: $files_with_pinvoke" +echo "Files with GC.KeepAlive: $files_with_keepalive" +echo "Files needing work: $((files_with_pinvoke - files_with_keepalive))" + +echo "" +echo "=== Next Steps ===" +echo "1. Review methods listed above" +echo "2. For each method with reference type parameters:" +echo " - Add GC.KeepAlive(param) after the SkiaApi call" +echo " - Remember to add for ALL reference type parameters" +echo "3. Test compilation after changes" +echo "4. Run unit tests to verify no regressions"