documentation/memory-management.md
Understanding pointer types is critical - wrong type = memory leaks or crashes.
| Type | C++ | C API | C# | Cleanup | Examples |
|---|---|---|---|---|---|
| Raw | const SkType& params | Pass through | owns: false | None | Paint in DrawRect() |
| Owned | new SkType() | sk_type_new/delete | DisposeNative() calls delete | Explicit delete | SKCanvas, SKPaint, SKPath |
| Ref-counted | sk_sp<SkType> | sk_type_ref/unref | ISKReferenceCounted | Call unref | SKImage, SKShader, SKData |
Check the C++ class declaration:
SkRefCnt or SkRefCntBase? → Virtual ref-counted (ISKReferenceCounted)SkNVRefCnt<T>? → Non-virtual ref-counted (ISKNonVirtualReferenceCounted)Is it wrapped in sk_sp<T>?
├─ Yes → Is it SkRefCnt or SkNVRefCnt?
│ ├─ SkRefCnt → ISKReferenceCounted (virtual ref counting)
│ └─ SkNVRefCnt<T> → ISKNonVirtualReferenceCounted
└─ No → Is it a parameter or getter return?
├─ Yes → Raw pointer (owns: false)
└─ No → Owned (DisposeNative deletes)
| Category | Types |
|---|---|
| Owned | SKCanvas, SKPaint, SKPath, SKBitmap, SKRegion |
| Ref-counted (virtual) | SKImage, SKShader, SKSurface, SKPicture, SKColorFilter, SKTypeface |
| Ref-counted (non-virtual) | SKData, SKTextBlob, SKVertices, SKColorSpace |
Borrowed references - caller or parent owns the object.
// C API - just pass through
SK_C_API void sk_canvas_draw_paint(sk_canvas_t* canvas, const sk_paint_t* paint) {
AsCanvas(canvas)->drawPaint(*AsPaint(paint));
}
// C# - create non-owning wrapper
public SKSurface Surface {
get {
var handle = SkiaApi.sk_canvas_get_surface(Handle);
return GetOrAddObject(handle, owns: false, (h, o) => new SKSurface(h, o));
}
}
Single owner, explicit delete on dispose.
// C API - new/delete pairs
SK_C_API sk_paint_t* sk_paint_new(void) { return ToPaint(new SkPaint()); }
SK_C_API void sk_paint_delete(sk_paint_t* paint) { delete AsPaint(paint); }
// C# - DisposeNative calls delete
public class SKPaint : SKObject {
public SKPaint() : base(SkiaApi.sk_paint_new(), true) { }
protected override void DisposeNative() => SkiaApi.sk_paint_delete(Handle);
}
Shared ownership via ref counting. Two variants:
SkRefCnt): SKImage, SKShader, SKSurface - use ISKReferenceCountedSkNVRefCnt<T>): SKData, SKTextBlob - use ISKNonVirtualReferenceCounted// C API - ref/unref functions, use sk_ref_sp when C++ expects sk_sp<T>
SK_C_API sk_image_t* sk_image_new_from_encoded(const sk_data_t* data) {
return ToImage(SkImages::DeferredFromEncodedData(sk_ref_sp(AsData(data))).release());
}
// C# - implements ISKReferenceCounted, disposal calls unref
public class SKImage : SKObject, ISKReferenceCounted {
public static SKImage FromEncodedData(SKData data) {
if (data == null) throw new ArgumentNullException(nameof(data));
return GetObject(SkiaApi.sk_image_new_from_encoded(data.Handle));
}
}
The HandleDictionary maintains a mapping from native IntPtr handles to C# wrapper objects.
Why needed:
How it works:
// WRONG: delete on ref-counted type
SK_C_API void sk_image_destroy(sk_image_t* image) { delete AsImage(image); }
// CORRECT: unref
SK_C_API void sk_image_unref(const sk_image_t* image) { SkSafeUnref(AsImage(image)); }
// WRONG: no ref increment
return ToImage(SkImages::Make(AsData(data)).release());
// CORRECT: sk_ref_sp increments ref count
return ToImage(SkImages::Make(sk_ref_sp(AsData(data))).release());
// WRONG: will destroy surface owned by canvas
return new SKSurface(handle, true);
// CORRECT: non-owning wrapper
return GetOrAddObject(handle, owns: false, ...);
// WRONG: sk_sp destructor decrements ref to 0
return ToImage(image);
// CORRECT: .release() transfers ownership
return ToImage(image.release());
fixed// WRONG: native object outlives the fixed block, GC moves the array
fixed (byte* ptr = data)
{
blob = new Blob(ptr, data.Length); // blob stores ptr
} // ptr invalid here — data can be moved/collected
// CORRECT: stable pin for native-retained pointers
var handle = GCHandle.Alloc(data, GCHandleType.Pinned);
var ptr = handle.AddrOfPinnedObject();
blob = new Blob(ptr, data.Length, () => handle.Free());
Known affected API: HarfBuzzSharp.Blob.FromStream — passes temporary fixed pointer to native HarfBuzz which stores it, leading to data corruption under GC pressure.
| Question | Answer |
|---|---|
| Inherits SkRefCnt? | Ref-counted → ISKReferenceCounted |
| Inherits SkNVRefCnt? | Ref-counted → ISKNonVirtualReferenceCounted |
| Mutable (Canvas/Paint)? | Owned → DisposeNative() calls delete |
| Parameter/getter? | Raw → owns: false |