Key principle: C# is the safety boundary. C API is a minimal pass-through.
| Layer | Responsibility |
|---|---|
| C# | Validates parameters, checks returns, throws exceptions |
| C API | Minimal wrapper, trusts C#, returns bool/null on failure |
| C++ | Native Skia, may throw (but rarely does) |
C# validates before P/Invoke. C API does NOT validate.
// C# - validates
public void DrawRect(SKRect rect, SKPaint paint) {
if (paint == null)
throw new ArgumentNullException(nameof(paint));
SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle);
}// C API - trusts C#, no validation
void sk_canvas_draw_rect(sk_canvas_t* canvas, const sk_rect_t* rect, const sk_paint_t* paint) {
AsCanvas(canvas)->drawRect(*AsRect(rect), *AsPaint(paint));
}Factory methods return null on failure - they do NOT throw.
public static SKImage FromEncodedData(SKData data) {
if (data == null)
throw new ArgumentNullException(nameof(data));
var handle = SkiaApi.sk_image_new_from_encoded(data.Handle);
return GetObject(handle); // Returns null if handle is IntPtr.Zero
}
// Caller MUST check for null
var image = SKImage.FromEncodedData(data);
if (image == null)
throw new InvalidOperationException("Failed to decode image");Methods that can fail return bool.
// C API - passes through C++ bool return
bool sk_bitmap_try_alloc_pixels(sk_bitmap_t* bitmap, const sk_imageinfo_t* info) {
return AsBitmap(bitmap)->tryAllocPixels(AsImageInfo(info));
}// C# - provide both Try and throwing versions
public bool TryAllocPixels(SKImageInfo info) {
return SkiaApi.sk_bitmap_try_alloc_pixels(Handle, &info);
}
public void AllocPixels(SKImageInfo info) {
if (!TryAllocPixels(info))
throw new InvalidOperationException("Failed to allocate pixels");
}Constructors throw on failure (unlike factory methods).
public SKBitmap(SKImageInfo info) : base(IntPtr.Zero, true) {
Handle = SkiaApi.sk_bitmap_new();
if (Handle == IntPtr.Zero)
throw new InvalidOperationException("Failed to create bitmap");
if (!SkiaApi.sk_bitmap_try_alloc_pixels(Handle, &info)) {
SkiaApi.sk_bitmap_destructor(Handle);
Handle = IntPtr.Zero;
throw new InvalidOperationException("Failed to allocate pixels");
}
}- Missing null checks in C# → crash in native code
- Not checking factory return values →
SKImage.FromEncodedData()returns null on failure, not exception - Assuming constructors return null → constructors throw, factory methods return null
Never throw from Dispose() or DisposeNative():
protected override void DisposeNative()
{
// Never throw from dispose - swallow exceptions if needed
if (Handle != IntPtr.Zero)
SkiaApi.sk_paint_delete(Handle);
}| Exception | When |
|---|---|
ArgumentNullException |
Null parameter |
ArgumentOutOfRangeException |
Value out of range |
ObjectDisposedException |
Operation on disposed object |
InvalidOperationException |
Operation failed |
| Scenario | C API | C# |
|---|---|---|
| Void method | Direct call | Validate params first |
| Bool return | Pass through | Return or throw based on method name |
| Pointer return | Return nullptr on fail | Factory→null, Constructor→throw |