This guide defines patterns for creating consistent, stable APIs in SkiaSharp.
- ABI Stability - Never break existing code
- Consistency - Follow established patterns
- Safety - Validate early, fail predictably
- Discoverability - APIs should be easy to find and use
SkiaSharp maintains stable ABI across versions. Breaking changes break downstream applications at runtime without recompilation.
| Change Type | Example |
|---|---|
| ✅ Add new overloads | CreateCopy(byte[] bytes, ulong length) |
| ✅ Add new methods | NewFeatureMethod() |
| ✅ Add new classes | public class SKNewFeature |
| ✅ Add new properties | public bool NewProperty { get; } |
| ✅ Add optional interfaces | Class can implement new interface |
| Change Type | Why It Breaks |
|---|---|
| ❌ Modify existing signatures | Callers compiled against old signature fail |
| ❌ Remove public APIs | Even deprecated - breaks callers |
| ❌ Change return types | Even to derived types - ABI mismatch |
| ❌ Change parameter types | Breaks overload resolution |
| ❌ Reorder parameters | Same signature, different meaning |
| ❌ Add required parameters | Existing callers don't provide them |
When an API needs to be replaced, use [Obsolete] but never remove:
// ✅ CORRECT - Add obsolete, provide alternative
[Obsolete("Use ToShader(SKShaderTileMode tmx, SKShaderTileMode tmy, SKSamplingOptions sampling) instead.")]
public SKShader ToShader(SKShaderTileMode tmx, SKShaderTileMode tmy) =>
ToShader(tmx, tmy, SKSamplingOptions.Default);
// Add the new preferred method alongside
public SKShader ToShader(SKShaderTileMode tmx, SKShaderTileMode tmy, SKSamplingOptions sampling) =>
// implementation| Type | Convention | Examples |
|---|---|---|
| Classes | SK prefix + PascalCase |
SKCanvas, SKPaint, SKImage |
| Structs | SK prefix + PascalCase |
SKRect, SKPoint, SKColor |
| Enums | SK prefix + PascalCase |
SKBlendMode, SKFilterMode |
| Interfaces | ISK prefix |
ISKReferenceCounted |
| Member | Convention | Examples |
|---|---|---|
| Methods | PascalCase, verb phrase | DrawRect(), CreateFromData() |
| Properties | PascalCase, noun/adjective | Width, IsDisposed, Handle |
| Parameters | camelCase | sourceRect, destPoint, filterQuality |
| Private fields | camelCase | handle, isDisposed, referenceCount |
| Constants | PascalCase | DefaultDpi, MaxTextureSize |
| Prefix | When to Use | Returns on Failure |
|---|---|---|
Create |
Creates new instance | null |
From* |
Converts/wraps existing data | null |
Decode |
Parses formatted data | null |
Try* |
Operation that may fail | bool + out param |
// Create - new instance from parameters
public static SKSurface Create(SKImageInfo info);
// From* - convert existing data
public static SKImage FromEncodedData(SKData data);
public static SKData FromStream(Stream stream);
// Decode - parse formatted content
public static SKCodec DecodeStream(Stream stream);
// Try* - explicit failure handling
public bool TryAllocPixels(SKImageInfo info);Prefer overloads over optional/default parameters:
// ✅ PREFERRED - Overload chain (from SKData.CreateCopy)
public static SKData CreateCopy(byte[] bytes) =>
CreateCopy(bytes, (ulong)bytes.Length);
public static SKData CreateCopy(byte[] bytes, ulong length)
{
fixed (byte* b = bytes) {
return GetObject(SkiaApi.sk_data_new_with_copy(b, (IntPtr)length));
}
}
// ❌ AVOID - Default parameters
public static SKData CreateCopy(byte[] bytes, ulong length = 0)Why:
- Overloads are CLS-compliant (some languages don't support defaults)
- Adding overloads doesn't break ABI; changing defaults does
- Clearer what the "default" behavior is
Simpler overloads should delegate to the most complete version:
// Simplest - convenience for int
public static SKData CreateCopy(IntPtr bytes, int length) =>
CreateCopy(bytes, (ulong)length);
// Intermediate - convenience for long
public static SKData CreateCopy(IntPtr bytes, long length) =>
CreateCopy(bytes, (ulong)length);
// Most complete - core implementation
public static SKData CreateCopy(IntPtr bytes, ulong length)
{
if (!PlatformConfiguration.Is64Bit && length > UInt32.MaxValue)
throw new ArgumentOutOfRangeException(nameof(length));
return GetObject(SkiaApi.sk_data_new_with_copy((void*)bytes, (IntPtr)length));
}Avoid boolean parameters when the meaning isn't obvious at the call site:
// ❌ AVOID - Unclear what 'true' means
image.ToRasterImage(true);
// ✅ BETTER - Named parameter makes intent clear
image.ToRasterImage(ensurePixelData: true);If a method needs multiple boolean options, consider:
- Named parameters (C# callers can use them)
- Options enum with
[Flags] - Options struct/class
Standard parameter ordering:
- Required inputs (most important first)
- Optional inputs
- Out/ref parameters last
// Good order: source → destination → options
public bool ScalePixels(SKPixmap source, SKPixmap destination, SKSamplingOptions sampling);Factory methods (Create, From*, Decode) 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
}Constructors throw on failure (can't return null):
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");
}
}Validate parameters at the C# layer, not in C API:
public void DrawRect(SKRect rect, SKPaint paint)
{
// Validate BEFORE P/Invoke
if (paint == null)
throw new ArgumentNullException(nameof(paint));
// Call native - C API trusts us
SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle);
}| Exception | When |
|---|---|
ArgumentNullException |
Null parameter that cannot be null |
ArgumentOutOfRangeException |
Value outside valid range |
ArgumentException |
Other parameter validation failures |
ObjectDisposedException |
Operation on disposed object |
InvalidOperationException |
Operation failed (catch-all) |
Some Skia methods return the same instance as an optimization. Always check before disposing:
// ❌ WRONG - crashes if methods return same instance
using var source = GetImage();
var result = source.Subset(bounds);
return result; // source disposed, but result IS source!
// ✅ CORRECT - check first
var source = GetImage();
var result = source.Subset(bounds);
if (result != source)
source.Dispose();
return result;Methods that may return same instance:
Subset()- when subset equals full boundsToRasterImage()- when already a raster imageToRasterImage(false)- when already non-texture
When implementing factory methods that take ownership:
// When SKImage takes ownership of data, use owns: true
internal static SKImage FromPixels(IntPtr pixels, int length, Action<IntPtr> releaseProc)
{
// Data will be released when image is disposed
return GetObject(SkiaApi.sk_image_new_from_raster(...), owns: true);
}From .editorconfig:
- Indentation: Tabs (4 spaces wide) for
.csfiles - Braces: Allman style (new line before opening brace)
- Using var: Preferred everywhere (
csharp_style_var_elsewhere = true) - Final newline: Required
- Trailing whitespace: Trimmed
| Member | Style |
|---|---|
| Properties | Expression body ✅ |
| Indexers | Expression body ✅ |
| Accessors | Expression body ✅ |
| Methods | Expression body for simple ones ✅ |
| Constructors | Block body preferred |
| Operators | Expression body ✅ |
// Properties - expression body
public int Width => info.Width;
// Simple methods - expression body
public SKRect GetBounds() => new SKRect(0, 0, Width, Height);
// Complex methods - block body
public void Draw(SKCanvas canvas)
{
canvas.Save();
// multiple statements
canvas.Restore();
}
// Constructors - block body preferred
public SKBitmap(int width, int height)
{
Handle = SkiaApi.sk_bitmap_new();
TryAllocPixels(new SKImageInfo(width, height));
}The codebase is transitioning to nullable reference types:
binding/- Currently#nullable disable(older code)source/- Uses#nullable enable(newer code)
When adding new code, follow the pattern of the file you're editing.
| Use Struct When | Use Class When |
|---|---|
| Immutable value type | Mutable or has identity |
| Small (<16 bytes typically) | Contains native handle |
| No inheritance needed | Needs inheritance/polymorphism |
| Frequently allocated | Long-lived, shared instances |
SkiaSharp examples:
SKRect,SKPoint,SKColor→ Structs (immutable values)SKCanvas,SKPaint,SKImage→ Classes (native handle, lifecycle)
// Standard enum - mutually exclusive values
public enum SKBlendMode
{
Clear,
Src,
Dst,
// ...
}
// Flags enum - combinable values
[Flags]
public enum SKFontStyleSlant
{
None = 0,
Upright = 1,
Italic = 2,
Oblique = 4,
}Skia is NOT thread-safe. Document thread safety requirements clearly:
/// <summary>
/// Draws a rectangle on the canvas.
/// </summary>
/// <remarks>
/// This method is not thread-safe. Canvas operations must be performed
/// on a single thread or with external synchronization.
/// </remarks>
public void DrawRect(SKRect rect, SKPaint paint)Thread-safe objects: SKData, SKImage, SKShader, SKColorFilter (immutable)
NOT thread-safe: SKCanvas, SKPaint, SKPath, SKBitmap (mutable)
When designing a new API:
- Does it follow naming conventions (SK prefix, PascalCase)?
- Factory methods return null on failure?
- Constructors throw on failure?
- Parameters validated at C# layer?
- Overloads used instead of optional parameters?
- Same-instance returns handled correctly?
- Memory ownership clearly documented?
- Thread safety documented?
- ABI stable (no breaking changes)?
- Deprecated APIs use
[Obsolete](not removed)?