diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..68f4ea71f4 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,82 @@ +# GitHub Copilot Instructions for SkiaSharp + +This file provides context for AI coding assistants working on SkiaSharp. + +## Quick Start + +**For quick reference:** See **[AGENTS.md](../AGENTS.md)** - 2 minute overview + +**For practical guide:** See **[design/QUICKSTART.md](../design/QUICKSTART.md)** - 10 minute tutorial + +**For comprehensive docs:** See **[design/](../design/)** folder + +## Path-Specific Instructions + +AI assistants automatically load context based on file paths from `.github/instructions/`: + +- **C API Layer** (`externals/skia/src/c/`) → [c-api-layer.instructions.md](instructions/c-api-layer.instructions.md) +- **C# Bindings** (`binding/SkiaSharp/`) → [csharp-bindings.instructions.md](instructions/csharp-bindings.instructions.md) +- **Generated Code** (`*.generated.cs`) → [generated-code.instructions.md](instructions/generated-code.instructions.md) +- **Native Skia** (`externals/skia/`) → [native-skia.instructions.md](instructions/native-skia.instructions.md) +- **Tests** (`tests/`) → [tests.instructions.md](instructions/tests.instructions.md) +- **Samples** (`samples/`) → [samples.instructions.md](instructions/samples.instructions.md) +- **Documentation** (`*.md`) → [documentation.instructions.md](instructions/documentation.instructions.md) + +See [instructions/README.md](instructions/README.md) for details. + +## Documentation Index + +### Essential Reading +- **[AGENTS.md](../AGENTS.md)** - Quick reference (AI agents, quick lookup) +- **[design/QUICKSTART.md](../design/QUICKSTART.md)** - Practical tutorial (new contributors) +- **[design/README.md](../design/README.md)** - Documentation index + +### Architecture & Concepts +- **[design/architecture-overview.md](../design/architecture-overview.md)** - Three-layer architecture, design principles +- **[design/memory-management.md](../design/memory-management.md)** - **Critical:** Pointer types, ownership, lifecycle +- **[design/error-handling.md](../design/error-handling.md)** - Error propagation through layers + +### Contributor Guides +- **[design/adding-new-apis.md](../design/adding-new-apis.md)** - Complete step-by-step guide with examples +- **[design/layer-mapping.md](../design/layer-mapping.md)** - Type mappings and naming conventions + +## Core Principles + +### Memory Management +Three pointer types (see [memory-management.md](../design/memory-management.md)): +1. **Raw (Non-Owning)** - Parameters, borrowed refs → No cleanup +2. **Owned** - Canvas, Paint → Call delete on dispose +3. **Ref-Counted** - Image, Shader, Data → Call unref on dispose + +### Error Handling +- **C API:** Minimal wrapper, trusts C# validation +- **C#:** Validates ALL parameters, checks returns, throws exceptions + +### Layer Boundaries +- **C++ → C API:** Direct calls, type conversion +- **C API → C#:** P/Invoke, parameter validation + +## Build & Test + +```bash +# Build managed code only +dotnet cake --target=libs + +# Run tests +dotnet cake --target=tests + +# Download pre-built native libraries +dotnet cake --target=externals-download +``` + +## When In Doubt + +1. Check [QUICKSTART.md](../design/QUICKSTART.md) for common patterns +2. Find similar existing API and follow its pattern +3. See [design/](../design/) for comprehensive details +4. Verify pointer type carefully (most important!) +5. Test memory management thoroughly + +--- + +**Remember:** Three layers, three pointer types, C# is the safety boundary. diff --git a/.github/instructions/README.md b/.github/instructions/README.md new file mode 100644 index 0000000000..fed538020f --- /dev/null +++ b/.github/instructions/README.md @@ -0,0 +1,137 @@ +# Path-Specific Instructions for SkiaSharp + +This directory contains path-specific instruction files that provide targeted guidance for AI coding agents working on different parts of the SkiaSharp codebase. + +## Overview + +Path-specific instructions automatically apply based on the files being edited, ensuring AI assistants use appropriate patterns, rules, and best practices for each layer or component. + +## Instruction Files + +| File | Applies To | Key Focus | +|------|-----------|-----------| +| **[c-api-layer.instructions.md](c-api-layer.instructions.md)** | `externals/skia/include/c/`, `externals/skia/src/c/` | C API bridging layer - no exceptions, C types, error codes | +| **[csharp-bindings.instructions.md](csharp-bindings.instructions.md)** | `binding/SkiaSharp/` | C# wrappers - IDisposable, P/Invoke, validation, exceptions | +| **[generated-code.instructions.md](generated-code.instructions.md)** | `*.generated.cs` files | Generated code - don't edit manually, modify templates | +| **[native-skia.instructions.md](native-skia.instructions.md)** | `externals/skia/` (excluding C API) | Upstream Skia C++ - understanding only, pointer types | +| **[tests.instructions.md](tests.instructions.md)** | `tests/`, `*Tests.cs` | Test code - memory management, error cases, lifecycle | +| **[documentation.instructions.md](documentation.instructions.md)** | `design/`, `*.md` | Documentation - clear examples, architecture focus | +| **[samples.instructions.md](samples.instructions.md)** | `samples/` | Sample code - best practices, complete examples | + +## How It Works + +AI coding agents that support path-specific instructions (like GitHub Copilot, Cursor, etc.) will automatically load and apply the relevant instruction file based on the file paths you're working with. + +For example: +- Editing `externals/skia/src/c/sk_canvas.cpp` → Loads **c-api-layer.instructions.md** +- Editing `binding/SkiaSharp/SKCanvas.cs` → Loads **csharp-bindings.instructions.md** +- Editing `tests/SKCanvasTests.cs` → Loads **tests.instructions.md** + +## Key Benefits + +### 1. Layer-Specific Guidance +Each layer has unique requirements: +- **C API:** Never throw exceptions, use C types, handle errors with return codes +- **C# Bindings:** Always dispose, validate parameters, convert to C# exceptions +- **Tests:** Focus on memory management, error cases, lifecycle + +### 2. Automatic Context +AI assistants automatically understand: +- Which patterns to follow +- What mistakes to avoid +- How to handle special cases + +### 3. Consistency +Ensures all AI-generated code follows the same patterns across the codebase. + +## Critical Concepts Covered + +### Memory Management (All Layers) +- **Raw pointers** (non-owning) - No cleanup needed +- **Owned pointers** - One owner, explicit delete/dispose +- **Reference-counted** - Shared ownership, ref/unref + +### Error Handling (Per Layer) +- **C API:** Catch all exceptions, return bool/null, defensive null checks +- **C#:** Validate parameters, check returns, throw typed exceptions +- **Tests:** Verify proper exception handling + +### Best Practices +- Proper disposal in C# (`using` statements) +- Complete, self-contained examples in samples +- Memory leak testing in test code +- Clear documentation with examples + +## Usage Examples + +### For AI Assistants + +When working on different files: + +``` +# Editing C API layer +externals/skia/src/c/sk_canvas.cpp +→ Applies: Never throw exceptions, use SK_C_API, handle errors + +# Editing C# wrapper +binding/SkiaSharp/SKCanvas.cs +→ Applies: Validate parameters, use IDisposable, throw exceptions + +# Writing tests +tests/SKCanvasTests.cs +→ Applies: Use using statements, test disposal, verify no leaks +``` + +### For Contributors + +These files serve as quick reference guides for: +- Understanding layer-specific requirements +- Following established patterns +- Avoiding common mistakes + +## Maintaining Instructions + +### When to Update + +Update instruction files when: +- Patterns or best practices change +- New common mistakes are discovered +- Layer responsibilities change +- New tooling or generators are added + +### What to Include + +Each instruction file should cover: +- ✅ Critical rules and requirements +- ✅ Common patterns with code examples +- ✅ What NOT to do (anti-patterns) +- ✅ Error handling specifics +- ✅ Memory management patterns + +### What to Avoid + +Don't include in instruction files: +- ❌ Exhaustive API documentation +- ❌ Build/setup instructions (use main docs) +- ❌ Temporary workarounds +- ❌ Implementation details + +## Related Documentation + +For comprehensive guidance, see: +- **[AGENTS.md](../../AGENTS.md)** - High-level project overview for AI agents +- **[design/](../../design/)** - Detailed architecture documentation +- **[.github/copilot-instructions.md](../copilot-instructions.md)** - General AI assistant context + +## Integration with AI Tools + +These instructions integrate with: +- **GitHub Copilot** - Workspace instructions +- **Cursor** - .cursorrules and workspace context +- **Other AI assistants** - Supporting path-specific patterns + +## Summary + +Path-specific instructions ensure AI coding agents apply the right patterns in the right places, maintaining code quality and consistency across SkiaSharp's three-layer architecture. + +**Key Principle:** Different layers require different approaches - these instructions ensure AI assistants understand and apply the correct patterns for each context. diff --git a/.github/instructions/c-api-layer.instructions.md b/.github/instructions/c-api-layer.instructions.md new file mode 100644 index 0000000000..4a6ddc2904 --- /dev/null +++ b/.github/instructions/c-api-layer.instructions.md @@ -0,0 +1,193 @@ +--- +applyTo: "externals/skia/include/c/**/*.h,externals/skia/src/c/**/*.cpp" +--- + +# C API Layer Instructions + +You are working in the C API layer that bridges Skia C++ to managed C#. + +> **📚 Documentation:** +> - **Quick Start:** [design/QUICKSTART.md](../../design/QUICKSTART.md) +> - **Architecture:** [design/architecture-overview.md](../../design/architecture-overview.md) +> - **Memory Management:** [design/memory-management.md](../../design/memory-management.md) +> - **Error Handling:** [design/error-handling.md](../../design/error-handling.md) + +## Critical Rules + +- All functions must use C linkage: `SK_C_API` or `extern "C"` +- Use C-compatible types only (no C++ classes in signatures) +- **Trust C# to validate** - C API is a minimal wrapper +- **No exception handling needed** - Skia rarely throws, C# prevents invalid inputs +- **No parameter validation needed** - C# validates before calling +- Keep implementations simple and direct + +## Pointer Type Handling + +> **💡 See [design/memory-management.md](../../design/memory-management.md) for pointer type concepts.** +> Below are C API-specific patterns for each type. + +### Raw Pointers (Non-Owning) +```cpp +// Just pass through, no ref counting +SK_C_API sk_canvas_t* sk_canvas_get_surface(sk_canvas_t* canvas); +``` + +### Owned Pointers +```cpp +// Create/destroy pairs +SK_C_API sk_paint_t* sk_paint_new(void); +SK_C_API void sk_paint_delete(sk_paint_t* paint); +``` + +### Reference-Counted Pointers +```cpp +// Explicit ref/unref functions +SK_C_API void sk_image_ref(const sk_image_t* image); +SK_C_API void sk_image_unref(const sk_image_t* image); + +// When C++ expects sk_sp, use sk_ref_sp to increment ref count +SK_C_API sk_image_t* sk_image_apply_filter( + const sk_image_t* image, + const sk_imagefilter_t* filter) +{ + return ToImage(AsImage(image)->makeWithFilter( + sk_ref_sp(AsImageFilter(filter))).release()); +} +``` + +## Naming Conventions + +- **Functions:** `sk__` (e.g., `sk_canvas_draw_rect`) +- **Types:** `sk__t` (e.g., `sk_canvas_t`) +- Keep names consistent with C++ equivalents + +## Type Conversion + +Use macros from `sk_types_priv.h`: +```cpp +AsCanvas(sk_canvas_t*) → SkCanvas* +ToCanvas(SkCanvas*) → sk_canvas_t* +``` + +Dereference pointers to get references: +```cpp +AsCanvas(canvas)->drawRect(*AsRect(rect), *AsPaint(paint)); +``` + +## Memory Management + +- Document ownership transfer in function comments +- Provide explicit create/destroy or ref/unref pairs +- Never assume caller will manage memory unless documented + +## Error Handling Patterns (Actual Implementation) + +### Boolean Return - Pass Through +```cpp +// C++ method returns bool, C API passes it through +SK_C_API bool sk_bitmap_try_alloc_pixels(sk_bitmap_t* bitmap, const sk_imageinfo_t* info) { + return AsBitmap(bitmap)->tryAllocPixels(AsImageInfo(info)); +} +``` + +**Note:** C# validates `bitmap` and `info` are non-null before calling. + +### Null Return for Factory Failure +```cpp +// Returns nullptr if Skia factory fails +SK_C_API sk_surface_t* sk_surface_new_raster(const sk_imageinfo_t* info) { + auto surface = SkSurfaces::Raster(AsImageInfo(info)); + return ToSurface(surface.release()); +} +``` + +**Note:** C# checks for `IntPtr.Zero` and throws exception if null. + +### Void Methods - Direct Call +```cpp +// Simple pass-through - C# ensures valid parameters +SK_C_API 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)); +} +``` + +**Design:** C API trusts C# has validated all parameters. + +## Common Patterns + +### Simple Method Call +```cpp +SK_C_API void sk_canvas_clear(sk_canvas_t* canvas, sk_color_t color) { + AsCanvas(canvas)->clear(color); +} +``` + +### Property Getter +```cpp +SK_C_API int sk_image_get_width(const sk_image_t* image) { + return AsImage(image)->width(); +} +``` + +### Property Setter +```cpp +SK_C_API void sk_paint_set_color(sk_paint_t* paint, sk_color_t color) { + AsPaint(paint)->setColor(color); +} +``` + +## What NOT to Do + +❌ **Never throw exceptions:** +```cpp +// WRONG +SK_C_API void sk_function() { + throw std::exception(); // Will crash! +} +``` + +❌ **Don't use C++ types in signatures:** +```cpp +// WRONG +SK_C_API void sk_function(std::string name); + +// CORRECT +SK_C_API void sk_function(const char* name); +``` + +❌ **Don't add unnecessary validation:** +```cpp +// WRONG - C# already validated +SK_C_API void sk_canvas_draw_rect(sk_canvas_t* canvas, const sk_rect_t* rect, const sk_paint_t* paint) { + if (!canvas || !rect || !paint) // Unnecessary - C# validated + return; + AsCanvas(canvas)->drawRect(*AsRect(rect), *AsPaint(paint)); +} + +// CORRECT - trust C# validation +SK_C_API 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)); +} +``` + +❌ **Don't add try-catch unless truly necessary:** +```cpp +// Usually NOT needed - Skia rarely throws, C# validates inputs +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()); +} +``` + +**Current implementation philosophy:** Minimal C API layer, safety enforced in C#. + +## Documentation + +Document these in function comments: +- Ownership transfer (who owns returned pointers) +- Null parameter handling +- Error conditions +- Thread-safety implications diff --git a/.github/instructions/csharp-bindings.instructions.md b/.github/instructions/csharp-bindings.instructions.md new file mode 100644 index 0000000000..2ff577db4f --- /dev/null +++ b/.github/instructions/csharp-bindings.instructions.md @@ -0,0 +1,116 @@ +--- +applyTo: "binding/SkiaSharp/**/*.cs" +--- + +# C# Bindings Instructions + +You are working in the C# wrapper layer that provides .NET access to Skia via P/Invoke. + +> **📚 Documentation:** +> - **Quick Start:** [design/QUICKSTART.md](../../design/QUICKSTART.md) +> - **Architecture:** [design/architecture-overview.md](../../design/architecture-overview.md) +> - **Memory Management:** [design/memory-management.md](../../design/memory-management.md) +> - **Adding APIs:** [design/adding-new-apis.md](../../design/adding-new-apis.md) + +## Critical Rules + +- All `IDisposable` types MUST dispose native handles +- Use `SKObject` base class for handle management +- Never expose `IntPtr` directly in public APIs +- Always validate parameters before P/Invoke calls +- Check return values from C API + +## Pointer Type to C# Mapping + +> **💡 See [design/memory-management.md](../../design/memory-management.md) for pointer type concepts.** +> Below are C#-specific patterns for each type. + +### Raw Pointers (Non-Owning) +```csharp +// OwnsHandle = false, no disposal +public SKSurface Surface { + get { + var handle = SkiaApi.sk_canvas_get_surface(Handle); + return GetOrAddObject(handle, owns: false, (h, o) => new SKSurface(h, o)); + } +} +``` + +### Owned Pointers +```csharp +public class SKCanvas : SKObject +{ + public SKCanvas(SKBitmap bitmap) : base(IntPtr.Zero, true) + { + if (bitmap == null) + throw new ArgumentNullException(nameof(bitmap)); + Handle = SkiaApi.sk_canvas_new_from_bitmap(bitmap.Handle); + } + + protected override void DisposeNative() + { + SkiaApi.sk_canvas_destroy(Handle); + } +} +``` + +### Reference-Counted Pointers +```csharp +public class SKImage : SKObject, ISKReferenceCounted +{ + public static SKImage FromBitmap(SKBitmap bitmap) + { + if (bitmap == null) + throw new ArgumentNullException(nameof(bitmap)); + + var handle = SkiaApi.sk_image_new_from_bitmap(bitmap.Handle); + if (handle == IntPtr.Zero) + throw new InvalidOperationException("Failed to create image"); + + return GetObject(handle); // For ISKReferenceCounted + } +} +``` + +## Parameter Validation + +### Before P/Invoke +```csharp +public void DrawRect(SKRect rect, SKPaint paint) +{ + // 1. Validate parameters + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + + // 2. Check object state + if (Handle == IntPtr.Zero) + throw new ObjectDisposedException(nameof(SKCanvas)); + + // 3. Call native + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); +} +``` + +## Error Handling + +Convert C API errors to exceptions: +```csharp +public static SKImage FromEncodedData(SKData data) +{ + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var handle = SkiaApi.sk_image_new_from_encoded(data.Handle); + + if (handle == IntPtr.Zero) + throw new InvalidOperationException("Failed to decode image"); + + return GetObject(handle); +} +``` + +## What NOT to Do + +❌ **Don't expose IntPtr directly in public APIs** +❌ **Don't skip parameter validation** +❌ **Don't ignore return values** diff --git a/.github/instructions/documentation.instructions.md b/.github/instructions/documentation.instructions.md new file mode 100644 index 0000000000..10b38aebe8 --- /dev/null +++ b/.github/instructions/documentation.instructions.md @@ -0,0 +1,56 @@ +--- +applyTo: "design/**/*.md,*.md,!node_modules/**,!externals/**" +--- + +# Documentation Instructions + +You are working on project documentation. + +> **📚 Reference:** +> - **Documentation Index:** [design/README.md](../../design/README.md) +> - **Quick Start:** [design/QUICKSTART.md](../../design/QUICKSTART.md) + +## Documentation Standards + +- Use clear, concise language +- Include code examples where helpful +- Document memory management and ownership +- Explain pointer type implications +- Cover error handling patterns +- Optimize for AI readability + +## Code Examples Best Practices + +### Always Show Disposal + +```csharp +// ✅ Good - proper disposal +using (var paint = new SKPaint()) +{ + paint.Color = SKColors.Red; +} +``` + +### Include Error Handling + +```csharp +if (string.IsNullOrEmpty(path)) + throw new ArgumentException("Path cannot be null or empty"); +``` + +### Show Complete Context +Include all necessary using statements and complete, runnable examples. + +## Structure Guidelines + +- Use clear headings +- Include diagrams where helpful (ASCII, Mermaid) +- Provide complete examples through all layers +- Cross-reference related documents + +## What NOT to Document + +- Exhaustive API lists (use XML comments instead) +- Implementation details (focus on concepts) +- Temporary workarounds +- Platform-specific details (unless critical) diff --git a/.github/instructions/generated-code.instructions.md b/.github/instructions/generated-code.instructions.md new file mode 100644 index 0000000000..a66d7881ff --- /dev/null +++ b/.github/instructions/generated-code.instructions.md @@ -0,0 +1,44 @@ +--- +applyTo: "binding/SkiaSharp/**/*.generated.cs,binding/SkiaSharp/**/SkiaApi.generated.cs" +--- + +# Generated Code Instructions + +You are viewing or working near **GENERATED CODE**. + +> **⚠️ Important:** Do NOT manually edit generated files. See [design/adding-new-apis.md](../../design/adding-new-apis.md) for the proper process. + +## Critical Rules + +- ⛔ **DO NOT manually edit generated files** +- Look for generation markers/comments at the top of files +- To modify generated code, change the generation templates/configs instead +- Document generation source in commit messages + +## If You Need to Change Generated Code + +### Step 1: Find the Generator +Located in: `utils/SkiaSharpGenerator/` + +### Step 2: Modify Template or Config +```bash +# Regenerate after changes +dotnet run --project utils/SkiaSharpGenerator/SkiaSharpGenerator.csproj -- generate +``` + +### Step 3: Verify Output +- Check generated code matches expectations +- Ensure no unintended changes +- Test the modified bindings + +## What You CAN Do + +✅ **Add hand-written wrappers** in separate files +✅ **Add convenience overloads** in non-generated files +✅ **Reference generated code** from hand-written code + +## What You CANNOT Do + +❌ **Manually edit generated P/Invoke declarations** +❌ **Add custom logic to generated files** +❌ **Modify generated file directly** (changes will be lost) diff --git a/.github/instructions/native-skia.instructions.md b/.github/instructions/native-skia.instructions.md new file mode 100644 index 0000000000..80cb4ecf13 --- /dev/null +++ b/.github/instructions/native-skia.instructions.md @@ -0,0 +1,49 @@ +--- +applyTo: "externals/skia/include/**/*.h,externals/skia/src/**/*.cpp,!externals/skia/include/c/**,!externals/skia/src/c/**" +--- + +# Native Skia C++ Instructions + +You are viewing native Skia C++ code. This is **upstream code** and should generally **NOT be modified directly**. + +> **📚 Documentation:** +> - **Quick Start:** [design/QUICKSTART.md](../../design/QUICKSTART.md) +> - **Memory Management:** [design/memory-management.md](../../design/memory-management.md) - See pointer type identification +> - **Adding APIs:** [design/adding-new-apis.md](../../design/adding-new-apis.md) - How to create bindings + +## Understanding This Code + +- This is the source C++ library that SkiaSharp wraps +- Pay attention to pointer types in function signatures +- Note: `sk_sp` is a smart pointer with reference counting +- Note: Raw `T*` may be owning or non-owning (check docs/context) + +## Pointer Type Identification + +> **💡 See [design/memory-management.md](../../design/memory-management.md) for complete guide.** +> Quick reference below: + +### Smart Pointers (Ownership) +- **`sk_sp`** - Skia Smart Pointer (Reference Counted) +- **`std::unique_ptr`** - Unique Ownership + +### Reference Counting +- **`SkRefCnt`** base class → Reference counted +- Methods: `ref()` increment, `unref()` decrement + +### Raw Pointers +- **`const T*` or `const T&`** → Usually non-owning, read-only +- **`T*`** → Could be owning or non-owning (requires context) + +## If Creating Bindings + +1. Identify pointer type from C++ signature +2. Create C API wrapper in `externals/skia/src/c/` +3. Handle ownership transfer appropriately +4. Ensure exceptions can't escape to C boundary + +## What NOT to Do + +❌ **Don't modify upstream Skia code** unless contributing upstream +❌ **Don't assume pointer ownership** without checking +❌ **Don't create C API here** - use `externals/skia/src/c/` instead diff --git a/.github/instructions/samples.instructions.md b/.github/instructions/samples.instructions.md new file mode 100644 index 0000000000..d689be7b5a --- /dev/null +++ b/.github/instructions/samples.instructions.md @@ -0,0 +1,67 @@ +--- +applyTo: "samples/**/*.cs" +--- + +# Sample Code Instructions + +You are working on sample/example code. + +> **📚 Documentation:** +> - **Quick Start:** [design/QUICKSTART.md](../../design/QUICKSTART.md) - See best practices +> - **API Guide:** [design/adding-new-apis.md](../../design/adding-new-apis.md) + +## Sample Code Standards + +- Demonstrate **best practices** (always use `using` statements) +- Include **error handling** +- Show **complete, working examples** +- Keep code **simple and educational** +- **Comment** complex or non-obvious parts + +## Memory Management in Samples + +### Always Use Using Statements +```csharp +// ✅ Correct +using (var surface = SKSurface.Create(info)) +using (var canvas = surface.Canvas) +using (var paint = new SKPaint()) +{ + // Use objects +} +``` + +### Make Self-Contained +```csharp +using System; +using System.IO; +using SkiaSharp; + +public static void DrawRectangleSample() +{ + var info = new SKImageInfo(256, 256); + + using (var surface = SKSurface.Create(info)) + using (var canvas = surface.Canvas) + using (var paint = new SKPaint { Color = SKColors.Blue }) + { + canvas.Clear(SKColors.White); + canvas.DrawRect(new SKRect(50, 50, 200, 200), paint); + + // Save + using (var image = surface.Snapshot()) + using (var data = image.Encode(SKEncodedImageFormat.Png, 100)) + using (var stream = File.OpenWrite("output.png")) + { + data.SaveTo(stream); + } + } +} +``` + +## What NOT to Do + +❌ **Don't skip disposal** +❌ **Don't show bad patterns** +❌ **Don't leave code incomplete** +❌ **Don't skip error handling** diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md new file mode 100644 index 0000000000..2a9b40c17e --- /dev/null +++ b/.github/instructions/tests.instructions.md @@ -0,0 +1,75 @@ +--- +applyTo: "tests/**/*.cs,**/*Tests.cs,**/*Test.cs" +--- + +# Test Code Instructions + +You are working on test code for SkiaSharp. + +> **📚 Documentation:** +> - **Quick Start:** [design/QUICKSTART.md](../../design/QUICKSTART.md) +> - **Memory Management:** [design/memory-management.md](../../design/memory-management.md) +> - **Error Handling:** [design/error-handling.md](../../design/error-handling.md) + +## Testing Focus Areas + +1. **Memory Management** - Verify no leaks, proper disposal, ref counting +2. **Error Handling** - Test invalid inputs, failure cases, exceptions +3. **Object Lifecycle** - Test create → use → dispose pattern +4. **Threading** - Test thread-safety where documented + +## Test Patterns + +### Always Use Using Statements +```csharp +[Fact] +public void DrawRectWorksCorrectly() +{ + using (var bitmap = new SKBitmap(100, 100)) + using (var canvas = new SKCanvas(bitmap)) + using (var paint = new SKPaint { Color = SKColors.Red }) + { + canvas.DrawRect(new SKRect(10, 10, 90, 90), paint); + Assert.NotEqual(SKColors.White, bitmap.GetPixel(50, 50)); + } +} +``` + +### Test Disposal +```csharp +[Fact] +public void DisposedObjectThrows() +{ + var paint = new SKPaint(); + paint.Dispose(); + Assert.Throws(() => paint.Color = SKColors.Red); +} +``` + +### Test Error Cases +```csharp +[Fact] +public void NullParameterThrows() +{ + using (var canvas = new SKCanvas(bitmap)) + { + Assert.Throws(() => + canvas.DrawRect(rect, null)); + } +} +``` + +## What to Test + +✅ Test both success and failure paths +✅ Test edge cases (empty, null, zero, negative, max) +✅ Verify exception types and messages +✅ Test complete lifecycle +✅ Test memory management (no leaks) + +## What NOT to Do + +❌ Leave objects undisposed in tests +❌ Ignore exception types +❌ Test only happy path +❌ Assume GC will clean up diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..5e9326d8d9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,182 @@ +# SkiaSharp - AGENTS.md + +## Project Overview + +SkiaSharp is a cross-platform 2D graphics API for .NET that wraps Google's Skia Graphics Library. It uses a three-layer architecture to bridge native C++ code with managed C#. + +**Key principle:** C++ exceptions cannot cross the C API boundary - all error handling uses return values. + +## Architecture + +### Three-Layer Design +``` +C# Wrapper Layer (binding/SkiaSharp/) + ↓ P/Invoke +C API Layer (externals/skia/include/c/, externals/skia/src/c/) + ↓ Type casting +C++ Skia Library (externals/skia/) +``` + +**Call flow example:** +``` +SKCanvas.DrawRect() → sk_canvas_draw_rect() → SkCanvas::drawRect() +``` + +## Critical Concepts + +### Memory Management - Pointer Types + +Three pointer types with different ownership rules: +- **Raw (`T*`)**: Non-owning, no cleanup needed +- **Owned**: Single owner, caller deletes (Canvas, Paint, Path) +- **Reference-Counted**: Shared ownership with ref counting (Image, Shader, Data) + +**Critical:** Wrong pointer type = memory leaks or crashes. + +👉 **Full details:** [design/memory-management.md](design/memory-management.md) + +### Error Handling + +- **C# validates all parameters** before calling C API +- **C API is minimal wrapper** - no validation, trusts C# +- **Factory methods return null** on failure (do NOT throw) +- **Constructors throw** on failure + +👉 **Full details:** [design/error-handling.md](design/error-handling.md) + +## File Organization + +### Naming Convention +``` +C++: SkCanvas.h → C API: sk_canvas.h, sk_canvas.cpp → C#: SKCanvas.cs +Pattern: SkType → sk_type_t* → SKType +``` + +### Key Directories + +**Do Not Modify:** +- `docs/` - Auto-generated API documentation + +**Core Areas:** +- `externals/skia/include/c/` - C API headers +- `externals/skia/src/c/` - C API implementation +- `binding/SkiaSharp/` - C# wrappers and P/Invoke +- `design/` - Architecture documentation (comprehensive guides) + +## Adding New APIs - Quick Steps + +1. Find C++ API in Skia +2. Identify pointer type (raw/owned/ref-counted) +3. Add C API wrapper (minimal, no validation) +4. Add C API header +5. Add P/Invoke declaration +6. Add C# wrapper with validation + +👉 **Full step-by-step guide:** [design/adding-new-apis.md](design/adding-new-apis.md) + +## Common Pitfalls + +❌ Wrong pointer type → memory leaks/crashes +❌ Missing ref count increment when C++ expects `sk_sp` +❌ Disposing borrowed objects +❌ Not checking factory method null returns +❌ Missing parameter validation in C# + +👉 **Full list with solutions:** [design/memory-management.md#common-pitfalls](design/memory-management.md#common-pitfalls) and [design/error-handling.md#common-mistakes](design/error-handling.md#common-mistakes) + +## Code Generation + +- **Hand-written:** C API layer (all `.cpp` in `externals/skia/src/c/`) +- **Generated:** Some P/Invoke declarations (`SkiaApi.generated.cs`) +- **Tool:** `utils/SkiaSharpGenerator/` + +To regenerate: +```bash +dotnet run --project utils/SkiaSharpGenerator/SkiaSharpGenerator.csproj -- generate +``` + +## Testing Checklist + +- [ ] Pointer type correctly identified +- [ ] Memory properly managed (no leaks) +- [ ] Object disposes correctly +- [ ] Error cases handled (null params, failed operations) +- [ ] P/Invoke signature matches C API +- [ ] Parameters validated in C# +- [ ] Return values checked + +## Threading + +**⚠️ Skia is NOT thread-safe** - Most objects must be used from a single thread. + +| Type | Thread-Safe? | Notes | +|------|--------------|-------| +| **Canvas/Paint/Path** | ❌ No | Keep thread-local | +| **Image/Shader/Typeface** | ✅ Yes* | Read-only after creation | + +*Immutable objects can be shared across threads. + +👉 **Full threading guide:** [design/architecture-overview.md#threading-model-and-concurrency](design/architecture-overview.md#threading-model-and-concurrency) + +## Build Commands + +```bash +# Build managed code only (after downloading native bits) +dotnet cake --target=libs + +# Run tests +dotnet cake --target=tests + +# Download pre-built native libraries +dotnet cake --target=externals-download +``` + +## Documentation + +**Quick reference:** This file + code comments + +**Practical tutorial:** [design/QUICKSTART.md](design/QUICKSTART.md) - 10-minute walkthrough + +**Detailed guides** in `design/` folder: +- `QUICKSTART.md` - **Start here!** Practical end-to-end tutorial +- `architecture-overview.md` - Three-layer architecture, design principles +- `memory-management.md` - **Critical**: Pointer types, ownership, lifecycle +- `error-handling.md` - Error propagation patterns through layers +- `adding-new-apis.md` - Complete step-by-step guide with examples +- `layer-mapping.md` - Type mappings and naming conventions + +**AI assistant context:** `.github/copilot-instructions.md` + +## Quick Decision Trees + +**"What pointer type?"** +Inherits SkRefCnt/SkNVRefCnt? → Reference-counted +Mutable (Canvas/Paint)? → Owned +Parameter/getter? → Raw (non-owning) + +**"What wrapper pattern?"** +Reference-counted → `ISKReferenceCounted` or `ISKNonVirtualReferenceCounted` +Owned → `SKObject` with `DisposeNative()` +Raw → `owns: false` in handle + +**"How to handle errors?"** +C API → Minimal pass-through; no extra exception handling, returns whatever Skia returns (bool/null/void) +C# → Validate where needed, but some APIs propagate null/bool/default results instead of throwing + +👉 **See also:** [design/adding-new-apis.md#decision-flowcharts](design/adding-new-apis.md#decision-flowcharts) + +## Examples + +See [design/adding-new-apis.md](design/adding-new-apis.md) for complete examples with all three layers. + +## When In Doubt + +1. Find similar existing API and follow its pattern +2. Check `design/` documentation for detailed guidance +3. Verify pointer type carefully (most important!) +4. Test memory management thoroughly +5. Ensure error handling at all layers + +--- + +**Remember:** Three layers, three pointer types, C# is the safety boundary. diff --git a/design/QUICKSTART.md b/design/QUICKSTART.md new file mode 100644 index 0000000000..67e79be696 --- /dev/null +++ b/design/QUICKSTART.md @@ -0,0 +1,545 @@ +# SkiaSharp Quick Start Guide + +**Goal:** Get you productive with SkiaSharp development in 10 minutes. + +This guide shows you **how to add a new API** from start to finish. For comprehensive reference, see the detailed documentation in this folder. + +## Table of Contents + +1. [Understanding the Three Layers](#understanding-the-three-layers) +2. [Identifying Pointer Types](#identifying-pointer-types) +3. [Adding a Simple API](#adding-a-simple-api-walkthrough) +4. [Error Handling Patterns](#error-handling-patterns) +5. [Common Mistakes](#top-10-common-mistakes) +6. [Next Steps](#next-steps) + +--- + +## Understanding the Three Layers + +SkiaSharp uses a three-layer architecture: + +> **📚 Deep Dive:** See [architecture-overview.md](architecture-overview.md) for complete architecture details. + +```mermaid +graph TB + subgraph CSharp["C# Layer (binding/SkiaSharp/)"] + CS1[Public .NET API] + CS2[SKCanvas, SKPaint, SKImage classes] + CS3[Validates parameters, throws exceptions] + end + + subgraph CAPI["C API Layer (externals/skia/src/c/)"] + C1[C functions: sk_canvas_draw_rect] + C2[Minimal wrapper - trusts C#] + C3[Returns bool/null for errors] + end + + subgraph CPP["C++ Layer (externals/skia/)"] + CPP1[Native Skia library] + CPP2[SkCanvas::drawRect] + CPP3[Can throw exceptions] + end + + CSharp -->|P/Invoke| CAPI + CAPI -->|Type casting
AsCanvas/ToCanvas| CPP + + style CSharp fill:#e1f5e1 + style CAPI fill:#fff4e1 + style CPP fill:#e1e8f5 +``` + +**Key principle:** C++ exceptions **cannot cross** the C API boundary. The C API layer catches all exceptions. + +--- + +## Identifying Pointer Types + +**Most important decision:** What pointer type does the API use? + +### Decision Flowchart + +> **💡 Tip:** See [memory-management.md](memory-management.md) for comprehensive pointer type details. + +```mermaid +graph TD + Start[Check C++ class declaration] --> Q1{Inherits SkRefCnt
or SkRefCntBase?} + Q1 -->|Yes| VirtRC[Virtual Ref-Counted
ISKReferenceCounted] + Q1 -->|No| Q2{Inherits
SkNVRefCnt<T>?} + Q2 -->|Yes| NonVirtRC[Non-Virtual Ref-Counted
ISKNonVirtualReferenceCounted] + Q2 -->|No| Q3{Mutable class?
Canvas, Paint, etc.} + Q3 -->|Yes| Owned[Owned Pointer
delete on dispose] + Q3 -->|No| Raw[Raw Pointer
Non-Owning] + + VirtRC -.->|Examples| VirtEx[SKImage, SKShader,
SKSurface, SKPicture] + NonVirtRC -.->|Examples| NonVirtEx[SKData, SKTextBlob,
SKVertices] + Owned -.->|Examples| OwnedEx[SKCanvas, SKPaint,
SKBitmap, SKPath] + Raw -.->|Examples| RawEx[Parameters,
borrowed refs] + + style VirtRC fill:#e1f5e1 + style NonVirtRC fill:#e1f5e1 + style Owned fill:#fff4e1 + style Raw fill:#e1e8f5 +``` + +### Quick Reference + +| Pointer Type | C++ | C API | C# | Cleanup | +|--------------|-----|-------|-----|---------| +| **Raw (Non-Owning)** | `const SkType&` parameter | Pass through | `owns: false` | None | +| **Owned** | `new SkType()` | `sk_type_new/delete()` | `DisposeNative() → delete` | Call delete | +| **Ref-Counted (Virtual)** | `: SkRefCnt` | `sk_type_ref/unref()` | `ISKReferenceCounted` | Call unref | +| **Ref-Counted (NV)** | `: SkNVRefCnt` | `sk_data_ref/unref()` | `ISKNonVirtualReferenceCounted` | Call type-specific unref | + +--- + +## Adding a Simple API: Walkthrough + +Let's add `SkCanvas::drawCircle()` to SkiaSharp. + +### Step 1: Find the C++ API + +**Location:** `externals/skia/include/core/SkCanvas.h` + +```cpp +class SkCanvas { +public: + void drawCircle(SkScalar cx, SkScalar cy, SkScalar radius, const SkPaint& paint); +}; +``` + +**Analysis:** +- Method on `SkCanvas` (owned pointer type) +- Parameters: `cx`, `cy`, `radius` are simple values, `paint` is const reference (borrowed) +- Returns: `void` (no error signaling) +- Cannot fail (simple drawing operation) + +### Step 2: Add C API Function + +**Location:** `externals/skia/src/c/sk_canvas.cpp` + +```cpp +void sk_canvas_draw_circle( + sk_canvas_t* canvas, + float cx, + float cy, + float radius, + const sk_paint_t* paint) +{ + // Call C++ method directly - C# ensures valid parameters + AsCanvas(canvas)->drawCircle(cx, cy, radius, *AsPaint(paint)); +} +``` + +**Key points:** +- Function name: `sk__` pattern +- **No validation needed** - C API trusts C# to pass valid parameters +- `AsCanvas()` and `AsPaint()` convert opaque pointers to C++ types +- Dereference with `*` to convert pointer to reference + +### Step 3: Add C API Header + +**Location:** `externals/skia/include/c/sk_canvas.h` + +```cpp +SK_C_API void sk_canvas_draw_circle( + sk_canvas_t* canvas, + float cx, + float cy, + float radius, + const sk_paint_t* paint); +``` + +### Step 4: Add P/Invoke Declaration + +**Location:** `binding/SkiaSharp/SkiaApi.cs` + +```csharp +[DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] +public static extern void sk_canvas_draw_circle( + sk_canvas_t canvas, + float cx, + float cy, + float radius, + sk_paint_t paint); +``` + +**Key points:** +- Use `sk_canvas_t` and `sk_paint_t` type aliases (defined as `IntPtr`) +- Match C API signature exactly +- Use `CallingConvention.Cdecl` + +### Step 5: Add C# Wrapper + +**Location:** `binding/SkiaSharp/SKCanvas.cs` + +```csharp +public void DrawCircle(float cx, float cy, float radius, SKPaint paint) +{ + // Validate parameters + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + + // Call P/Invoke + SkiaApi.sk_canvas_draw_circle(Handle, cx, cy, radius, paint.Handle); +} +``` + +**Key points:** +- Use .NET naming conventions (PascalCase) +- Validate parameters before P/Invoke +- Use `Handle` property to get native pointer +- No need to check return value (void function) + +### Done! ✅ + +You've added a complete binding across all three layers. + +--- + +## Error Handling Patterns + +> **📚 Deep Dive:** See [error-handling.md](error-handling.md) for comprehensive error handling patterns. + +### Pattern 1: Boolean Return (Try Methods) + +**C++ (can throw):** +```cpp +bool SkBitmap::tryAllocPixels(const SkImageInfo& info); +``` + +**C API (pass through):** +```cpp +bool sk_bitmap_try_alloc_pixels(sk_bitmap_t* bitmap, const sk_imageinfo_t* info) { + return AsBitmap(bitmap)->tryAllocPixels(AsImageInfo(info)); +} +``` + +**C# (throw on false):** +```csharp +public bool TryAllocPixels(SKImageInfo info) +{ + var nInfo = SKImageInfoNative.FromManaged(ref info); + return SkiaApi.sk_bitmap_try_alloc_pixels(Handle, &nInfo); +} + +public void AllocPixels(SKImageInfo info) +{ + if (!TryAllocPixels(info)) + throw new InvalidOperationException("Failed to allocate pixels"); +} +``` + +### Pattern 2: Null Return (Factory Methods) + +**C++ (returns nullptr on failure):** +```cpp +sk_sp SkImages::DeferredFromEncodedData(sk_sp data); +``` + +**C API (pass through):** +```cpp +sk_image_t* sk_image_new_from_encoded(const sk_data_t* data) { + auto image = SkImages::DeferredFromEncodedData(sk_ref_sp(AsData(data))); + return ToImage(image.release()); +} +``` + +**C# (returns null, does NOT throw):** +```csharp +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 +} + +// ✅ CORRECT usage - check for null +var image = SKImage.FromEncodedData(data); +if (image == null) + throw new InvalidOperationException("Failed to decode image"); +``` + +**Note:** Factory methods return `null` on failure, they do NOT throw exceptions. Always check the return value. + +### Pattern 3: Void Methods (Minimal C API) + +**C API (no validation):** +```cpp +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)); +} +``` + +**C# (validates before calling):** +```csharp +public void DrawRect(SKRect rect, SKPaint paint) +{ + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); +} +``` + +**Design:** C API is a minimal wrapper with no validation. C# validates all parameters before P/Invoke. + +--- + +## Top 10 Common Mistakes + +### 1. ❌ Wrong Pointer Type +```csharp +// WRONG: SKImage is ref-counted, not owned +protected override void DisposeNative() +{ + SkiaApi.sk_image_delete(Handle); // No such function! +} + +// CORRECT: Use unref for ref-counted types +// SKImage implements ISKReferenceCounted, which handles this automatically +``` + +### 2. ❌ Passing NULL to C API (C# validation missing) + +```csharp +// WRONG: No validation - will crash in C API! +public void DrawRect(SKRect rect, SKPaint paint) +{ + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); // Crashes if paint is null +} + +// CORRECT: Validate in C# before calling C API +public void DrawRect(SKRect rect, SKPaint paint) +{ + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); +} +``` + +**Why this matters:** C API does NOT validate - it trusts C# to send valid pointers. + +### 3. ❌ Missing Parameter Validation +```csharp +// WRONG: No validation +public void DrawRect(SKRect rect, SKPaint paint) +{ + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); // paint could be null! +} + +// CORRECT: Validate first +public void DrawRect(SKRect rect, SKPaint paint) +{ + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); +} +``` + +### 4. ❌ Not Checking for Null Returns +```csharp +// WRONG: Factory methods can return null +var image = SKImage.FromEncodedData(data); +canvas.DrawImage(image, 0, 0); // NullReferenceException if decode failed! + +// CORRECT: Check for null +var image = SKImage.FromEncodedData(data); +if (image == null) + throw new InvalidOperationException("Failed to decode image"); +canvas.DrawImage(image, 0, 0); +``` + +**Important:** Static factory methods return `null` on failure, they do NOT throw exceptions! + +### 5. ❌ Missing sk_ref_sp for Ref-Counted Parameters +```cpp +// WRONG: C++ expects sk_sp, this doesn't increment ref count +sk_image_t* sk_image_new(const sk_data_t* data) { + return ToImage(SkImages::Make(AsData(data)).release()); // LEAK or CRASH! +} + +// CORRECT: Use sk_ref_sp to create sk_sp and increment ref +sk_image_t* sk_image_new(const sk_data_t* data) { + return ToImage(SkImages::Make(sk_ref_sp(AsData(data))).release()); +} +``` + +### 6. ❌ Using C++ Types in C API +```cpp +// WRONG: std::string is C++ +SK_C_API void sk_function(std::string name); + +// CORRECT: Use C types +SK_C_API void sk_function(const char* name); +``` + +### 7. ❌ Not Disposing IDisposable Objects +```csharp +// WRONG: Memory leak +var paint = new SKPaint(); +paint.Color = SKColors.Red; +// paint never disposed! + +// CORRECT: Use using statement +using (var paint = new SKPaint()) +{ + paint.Color = SKColors.Red; +} // Automatically disposed +``` + +### 8. ❌ Exposing IntPtr in Public API +```csharp +// WRONG: IntPtr is implementation detail +public IntPtr NativeHandle { get; } + +// CORRECT: Keep internal +internal IntPtr Handle { get; } +``` + +### 9. ❌ Missing Validation in C# (not C API) + +```csharp +// WRONG: No null parameter validation +public void DrawRect(SKRect rect, SKPaint paint) +{ + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); // Crash if paint is null! +} + +// CORRECT: Validate null reference parameters before calling C API +public void DrawRect(SKRect rect, SKPaint paint) +{ + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); +} +``` + +**Note:** Most instance methods do NOT check if the object is disposed (Handle == IntPtr.Zero). They assume the object is valid if the wrapper exists. The primary validation is null-checking reference parameters that would crash native code. + +**Remember:** C# is the safety boundary - validate parameters that would crash native code before P/Invoke! + +### 10. ❌ Forgetting .release() on sk_sp +```cpp +// WRONG: sk_sp will unref when destroyed, ref count goes to 0 +SK_C_API sk_image_t* sk_image_new() { + sk_sp image = SkImages::Make(...); + return ToImage(image); // Converted to raw pointer, then sk_sp destructs → CRASH! +} + +// CORRECT: .release() transfers ownership +SK_C_API sk_image_t* sk_image_new() { + sk_sp image = SkImages::Make(...); + return ToImage(image.release()); // Releases sk_sp ownership, ref count stays 1 +} +``` + +--- + +## Threading Quick Reference + +> **📚 Deep Dive:** See [architecture-overview.md - Threading Model](architecture-overview.md#threading-model-and-concurrency) for comprehensive threading documentation. + +### Thread Safety Matrix + +| Object Type | Thread-Safe? | Can Share? | Rule | +|-------------|--------------|------------|------| +| SKCanvas | ❌ No | No | One thread only | +| SKPaint | ❌ No | No | One thread only | +| SKPath | ❌ No | No | One thread only | +| SKImage | ✅ Yes (read-only) | Yes | Immutable, shareable | +| SKShader | ✅ Yes (read-only) | Yes | Immutable, shareable | +| SKTypeface | ✅ Yes (read-only) | Yes | Immutable, shareable | + +### Pattern: Background Rendering + +```csharp +// ✅ GOOD: Each thread has its own objects +var image = await Task.Run(() => +{ + using var surface = SKSurface.Create(info); + using var canvas = surface.Canvas; // Thread-local canvas + using var paint = new SKPaint(); // Thread-local paint + + canvas.Clear(SKColors.White); + canvas.DrawCircle(100, 100, 50, paint); + + return surface.Snapshot(); // Returns immutable SKImage (shareable) +}); + +// Safe to use image on UI thread +imageView.Image = image; +``` + +### Pattern: Shared Immutable Resources + +```csharp +// ✅ GOOD: Load once, share across threads +private static readonly SKTypeface _sharedFont = SKTypeface.FromFile("font.ttf"); +private static readonly SKImage _sharedLogo = SKImage.FromEncodedData("logo.png"); + +void DrawOnAnyThread(SKCanvas canvas) +{ + // Immutable objects can be safely shared + using var paint = new SKPaint { Typeface = _sharedFont }; + canvas.DrawImage(_sharedLogo, 0, 0); +} +``` + +### ❌ Common Threading Mistake + +```csharp +// WRONG: Sharing mutable objects across threads +SKCanvas sharedCanvas; // ❌ BAD! + +Task.Run(() => sharedCanvas.DrawRect(...)); // Thread 1 +Task.Run(() => sharedCanvas.DrawCircle(...)); // Thread 2 - RACE CONDITION! +``` + +**Remember:** Mutable objects (Canvas, Paint, Path) are NOT thread-safe. Keep them thread-local! + +--- + +## Next Steps + +### For Quick Reference +- **[AGENTS.md](../AGENTS.md)** - Ultra-quick lookup (2 minutes) + +### For Deep Dives +- **[architecture-overview.md](architecture-overview.md)** - Complete architecture details +- **[memory-management.md](memory-management.md)** - Everything about pointer types +- **[error-handling.md](error-handling.md)** - Complete error patterns +- **[layer-mapping.md](layer-mapping.md)** - Type mapping reference +- **[adding-new-apis.md](adding-new-apis.md)** - Comprehensive API guide + +### For Path-Specific Rules +- **[.github/instructions/](../.github/instructions/)** - Auto-loading instructions per file type + +### Testing Your Changes +```bash +# Build managed code +dotnet cake --target=libs + +# Run tests +dotnet cake --target=tests +``` + +--- + +## Summary + +**Remember:** +1. **Three layers:** C# → C API → C++ +2. **C# validates everything:** Parameters checked before P/Invoke +3. **Three pointer types:** Raw, Owned, Ref-counted +4. **Factory methods return null:** Always check for null returns +5. **Constructors throw:** On allocation/creation failures + +**When in doubt:** +- Check similar existing APIs +- Follow the patterns in this guide +- See comprehensive docs for details + +Good luck! 🎨 diff --git a/design/README.md b/design/README.md new file mode 100644 index 0000000000..55a904559d --- /dev/null +++ b/design/README.md @@ -0,0 +1,243 @@ +# SkiaSharp Design Documentation + +This folder contains comprehensive documentation for understanding and contributing to SkiaSharp. + +## 🚀 Start Here + +### For Quick Answers +- **[../AGENTS.md](../AGENTS.md)** - 2-minute quick reference (AI agents, quick lookup) + +### For Getting Started +- **[QUICKSTART.md](QUICKSTART.md)** - **⭐ Start here!** 10-minute practical tutorial + - How to add an API end-to-end + - Pointer type identification flowchart + - Common mistakes and how to avoid them + +### For Comprehensive Reference +Continue reading below for the complete documentation index. + +--- + +## Documentation Index + +### Getting Started + +0. **[QUICKSTART.md](QUICKSTART.md)** - **⭐ Practical tutorial (start here!)** + - Complete API addition walkthrough + - Pointer type decision flowchart + - Error handling patterns + - Top 10 common mistakes + - Quick examples for immediate productivity + +### Core Architecture Documents + +1. **[architecture-overview.md](architecture-overview.md)** - Three-layer architecture + - Three-layer architecture (C++ → C API → C#) + - How components connect + - Call flow examples + - File organization + - Code generation overview + - Key design principles + +2. **[memory-management.md](memory-management.md)** - Critical for correct bindings + - Three pointer type categories (raw, owned, reference-counted) + - How each type maps through layers + - Ownership semantics and lifecycle patterns + - How to identify pointer types from C++ signatures + - Common mistakes and how to avoid them + - Thread safety considerations + +3. **[error-handling.md](error-handling.md)** - Understanding error flow + - Error handling strategy by layer + - Validation patterns in C# + - Factory methods return null (not throw) + - Complete error flow examples + - Best practices and debugging tips + +4. **[adding-new-apis.md](adding-new-apis.md)** - Step-by-step contributor guide + - How to analyze C++ APIs + - Adding C API wrapper functions + - Creating P/Invoke declarations + - Writing C# wrapper code + - Testing your changes + - Complete examples and patterns + - Troubleshooting guide + +5. **[layer-mapping.md](layer-mapping.md)** - Quick reference + - Type naming conventions across layers + - Function naming patterns + - File organization mapping + - Type conversion macros + - Common API patterns + - Parameter passing patterns + +## Quick Start Guide + +### For AI Assistants (GitHub Copilot) + +See [../.github/copilot-instructions.md](../.github/copilot-instructions.md) for: +- Condensed context optimized for AI +- Quick decision trees +- Common patterns and anti-patterns +- Checklist for changes + +### For Human Contributors + +**First time working with SkiaSharp?** + +1. Read [architecture-overview.md](architecture-overview.md) to understand the three-layer structure +2. Study [memory-management.md](memory-management.md) to understand pointer types (critical!) +3. Review [error-handling.md](error-handling.md) to understand error propagation +4. When ready to add APIs, follow [adding-new-apis.md](adding-new-apis.md) +5. Keep [layer-mapping.md](layer-mapping.md) open as a reference + +**Want to understand existing code?** + +Use the documentation to trace through layers: +1. Start with C# API in `binding/SkiaSharp/SK*.cs` +2. Find P/Invoke in `SkiaApi.cs` or `SkiaApi.generated.cs` +3. Locate C API in `externals/skia/include/c/sk_*.h` +4. Check implementation in `externals/skia/src/c/sk_*.cpp` +5. Find C++ API in `externals/skia/include/core/Sk*.h` + +## Key Concepts at a Glance + +### The Three-Layer Architecture + +``` +C# Wrapper (binding/) → P/Invoke → C API (externals/skia/src/c/) → C++ Skia +``` + +**→ Full details:** [architecture-overview.md](architecture-overview.md) + +### Three Pointer Types + +| Type | Examples | Cleanup | +|------|----------|---------| +| **Raw** | Parameters, getters | None | +| **Owned** | Canvas, Paint | `delete` | +| **Ref-counted** | Image, Shader | `unref()` | + +**→ Full details:** [memory-management.md](memory-management.md) + +### Error Handling + +C++ throws → C API catches (returns bool/null) → C# checks & throws + +**→ Full details:** [error-handling.md](error-handling.md) + +## Use Cases Supported + +### Use Case 1: Understanding Existing Code + +> "I'm looking at `SKCanvas.DrawRect()` in C# - trace this call through all layers to understand how it reaches native Skia code and what memory management is happening." + +**Solution:** Follow the call flow in [architecture-overview.md](architecture-overview.md), check pointer types in [memory-management.md](memory-management.md) + +### Use Case 2: Understanding Pointer Types + +> "I see `sk_canvas_t*` in the C API and `SKCanvas` in C#. What pointer type does SKCanvas use in native Skia? How does this affect the C# wrapper's dispose pattern?" + +**Solution:** Check [memory-management.md](memory-management.md) section on "Owned Pointers" and "Identifying Pointer Types" + +### Use Case 3: Adding a New API + +> "Skia added a new `SkCanvas::drawArc()` method. What files do I need to modify in each layer, and how should I handle memory management?" + +**Solution:** Follow step-by-step guide in [adding-new-apis.md](adding-new-apis.md) + +### Use Case 4: Debugging Memory Issues + +> "There's a memory leak involving SKBitmap objects. How do I understand the lifecycle and pointer type to find where disposal or reference counting might be wrong?" + +**Solution:** Check [memory-management.md](memory-management.md) for lifecycle patterns and common mistakes + +### Use Case 5: Understanding Error Flow + +> "A native Skia operation failed but my C# code didn't catch any exception. How do errors flow through the layers, and where might error handling be missing?" + +**Solution:** Review [error-handling.md](error-handling.md) for error propagation patterns and debugging + +### Use Case 6: Working with Reference Counting + +> "SKImage seems to use reference counting. How does this work across the C API boundary? When do I need to call ref/unref functions?" + +**Solution:** See [memory-management.md](memory-management.md) section on "Reference-Counted Pointers" with examples + +## Documentation Maintenance + +### When to Update + +Update this documentation when: +- Adding new patterns or architectural changes +- Discovering common mistakes or gotchas +- Significant changes to memory management strategy +- Adding new pointer type categories +- Changing error handling approach + +### What NOT to Document Here + +Don't duplicate information that's better elsewhere: +- **API documentation** - Use XML comments in code, generated to `docs/` +- **Build instructions** - Keep in Wiki or root README +- **Version history** - Keep in CHANGELOG or release notes +- **Platform specifics** - Keep in platform-specific docs + +### Keep It Maintainable + +- Focus on architecture and patterns, not specific APIs +- Use examples that are unlikely to change +- Reference stable parts of the codebase +- Update when patterns change, not when APIs are added + +## Contributing to Documentation + +Improvements welcome! When contributing: + +1. **Keep it high-level** - Focus on concepts, not exhaustive API lists +2. **Add examples** - Show complete patterns through all three layers +3. **Optimize for searchability** - Use clear headings and keywords +4. **Test understanding** - Can someone follow your examples? +5. **Update cross-references** - Keep links between documents current + +## Additional Resources + +### External Documentation + +- **Skia Website:** https://skia.org/ +- **Skia C++ API Reference:** https://api.skia.org/ +- **SkiaSharp Wiki:** https://github.com/mono/SkiaSharp/wiki +- **SkiaSharp Samples:** https://github.com/mono/SkiaSharp/tree/main/samples + +### Related Documentation + +- **Root README.md** - Project overview and getting started +- **Wiki: Creating Bindings** - Original binding guide (less detailed) +- **Wiki: Building SkiaSharp** - Build instructions +- **Source XML Comments** - API-level documentation + +### Questions or Issues? + +- **Architecture questions:** Review this documentation first +- **Build issues:** Check Wiki or root README +- **API usage:** Check generated API docs in `docs/` +- **Bugs:** File an issue on GitHub + +## Version History + +- **2024-11-07:** Initial architecture documentation created + - Comprehensive coverage of three-layer architecture + - Detailed memory management with pointer types + - Complete error handling patterns + - Step-by-step API addition guide + - Layer mapping reference + - GitHub Copilot instructions + +--- + +**Remember:** The three most important concepts are: +1. **Three-layer architecture** (C++ → C API → C#) +2. **Three pointer types** (raw, owned, reference-counted) +3. **C# is safety boundary** (validates all, factory methods return null) + +Master these, and you'll understand SkiaSharp's design. diff --git a/design/adding-new-apis.md b/design/adding-new-apis.md new file mode 100644 index 0000000000..4884784d16 --- /dev/null +++ b/design/adding-new-apis.md @@ -0,0 +1,796 @@ +# Adding New APIs to SkiaSharp + +> **Quick Start:** For a quick walkthrough, see [QUICKSTART.md](QUICKSTART.md) +> **Quick Reference:** For common patterns, see [AGENTS.md](../AGENTS.md) + +## TL;DR + +**5-step process to add a new API:** + +1. **Find C++ API** - Locate in `externals/skia/include/core/` +2. **Identify pointer type** - Check inheritance: `SkRefCnt`, `SkNVRefCnt`, or owned +3. **Add C API** - Create wrapper in `externals/skia/src/c/` with exception handling +4. **Add P/Invoke** - Declare in `binding/SkiaSharp/SkiaApi.cs` +5. **Add C# wrapper** - Implement in `binding/SkiaSharp/SK*.cs` with validation + +**Critical decisions:** +- Pointer type (determines disposal pattern) +- Error handling (can it fail?) +- Parameter types (ref-counted need `sk_ref_sp`) + +**File locations:** +``` +C++: externals/skia/include/core/SkCanvas.h +C API: externals/skia/src/c/sk_canvas.cpp + externals/skia/include/c/sk_canvas.h +C#: binding/SkiaSharp/SKCanvas.cs + binding/SkiaSharp/SkiaApi.cs +``` + +See [QUICKSTART.md](QUICKSTART.md) for a complete example, or continue below for comprehensive details. + +--- + +## Introduction + +This guide walks through the complete process of adding a new Skia API to SkiaSharp, from identifying the C++ API to testing the final C# binding. + +## Prerequisites + +Before adding a new API, you should understand: +- [Architecture Overview](architecture-overview.md) - The three-layer structure +- [Memory Management](memory-management.md) - Pointer types and ownership +- [Error Handling](error-handling.md) - How errors propagate + +## Overview: The Four-Step Process + +``` +1. Analyze C++ API → Identify pointer type & error handling +2. Add C API Layer → Create C wrapper functions +3. Add P/Invoke → Declare C# interop +4. Add C# Wrapper → Create idiomatic C# API +``` + +## Step 1: Analyze the C++ API + +### Find the C++ API + +Locate the API in Skia's C++ headers: + +```bash +# Search Skia headers +grep -r "drawArc" externals/skia/include/core/ + +# Common locations: +# - externals/skia/include/core/SkCanvas.h +# - externals/skia/include/core/SkPaint.h +# - externals/skia/include/core/SkImage.h +``` + +**Example:** Let's add `SkCanvas::drawArc()` + +```cpp +// In SkCanvas.h +class SK_API SkCanvas { +public: + void drawArc(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle, + bool useCenter, const SkPaint& paint); +}; +``` + +### Determine Pointer Type and Ownership + +**Key questions to answer:** + +1. **What type of object is this method on?** + - `SkCanvas` → Owned pointer (mutable, not ref-counted) + +2. **What parameters does it take?** + - `const SkRect&` → Raw pointer (non-owning, value parameter) + - `const SkPaint&` → Raw pointer (non-owning, borrowed) + - `SkScalar` → Value type (primitive) + - `bool` → Value type (primitive) + +3. **Does it return anything?** + - `void` → No return value + +4. **Can it fail?** + - Drawing operations typically don't fail + - May clip or do nothing if parameters invalid + - No error return needed + +**Pointer type analysis:** +- Canvas: Owned (must exist for call duration) +- Paint: Borrowed (only used during call) +- Rect: Value (copied, safe to stack allocate) + +**See [Memory Management](memory-management.md) for detailed pointer type identification.** + +### Check Skia Documentation + +```cpp +// From SkCanvas.h comments: +/** Draws arc of oval bounded by oval_rect. + @param oval rect bounds of oval containing arc + @param startAngle starting angle in degrees + @param sweepAngle sweep angle in degrees + @param useCenter if true, include center of oval + @param paint paint to use +*/ +void drawArc(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle, + bool useCenter, const SkPaint& paint); +``` + +## Step 2: Add C API Layer + +### File Location + +Add to existing or create new C API files: +- Header: `externals/skia/include/c/sk_canvas.h` +- Implementation: `externals/skia/src/c/sk_canvas.cpp` + +### Add Function Declaration to Header + +```cpp +// In externals/skia/include/c/sk_canvas.h + +SK_C_API void sk_canvas_draw_arc( + sk_canvas_t* ccanvas, + const sk_rect_t* oval, + float startAngle, + float sweepAngle, + bool useCenter, + const sk_paint_t* cpaint); +``` + +**Naming convention:** +- Function: `sk__` +- Example: `sk_canvas_draw_arc` + +**Parameter types:** +- C++ `SkCanvas*` → C `sk_canvas_t*` +- C++ `const SkRect&` → C `const sk_rect_t*` +- C++ `SkScalar` → C `float` +- C++ `const SkPaint&` → C `const sk_paint_t*` +- C++ `bool` → C `bool` + +### Add Implementation + +```cpp +// In externals/skia/src/c/sk_canvas.cpp + +void sk_canvas_draw_arc( + sk_canvas_t* ccanvas, + const sk_rect_t* oval, + float startAngle, + float sweepAngle, + bool useCenter, + const sk_paint_t* cpaint) +{ + // Defensive null checks (optional but recommended) + if (!ccanvas || !oval || !cpaint) + return; + + // Convert C types to C++ types and call + AsCanvas(ccanvas)->drawArc( + *AsRect(oval), // Dereference pointer to get reference + startAngle, // SkScalar is float + sweepAngle, + useCenter, + *AsPaint(cpaint)); // Dereference pointer to get reference +} +``` + +**Key points:** +- Type conversion macros: `AsCanvas()`, `AsRect()`, `AsPaint()` +- Dereference pointers (`*`) to get C++ references +- Keep implementation simple - C# validates parameters +- No try-catch needed (C# prevents invalid inputs) + +### Special Cases + +#### Returning Owned Pointers + +```cpp +SK_C_API sk_canvas_t* sk_canvas_new_from_bitmap(const sk_bitmap_t* bitmap) { + // Create new canvas - caller owns + return ToCanvas(new SkCanvas(*AsBitmap(bitmap))); +} +``` + +#### Returning Reference-Counted Pointers + +```cpp +SK_C_API sk_image_t* sk_image_new_from_bitmap(const sk_bitmap_t* bitmap) { + // SkImages::RasterFromBitmap returns sk_sp + // .release() transfers ownership (ref count = 1) + return ToImage(SkImages::RasterFromBitmap(*AsBitmap(bitmap)).release()); +} +``` + +#### Taking Reference-Counted Parameters + +```cpp +SK_C_API sk_image_t* sk_image_apply_filter( + const sk_image_t* image, + const sk_imagefilter_t* filter) +{ + // Filter is ref-counted, C++ wants sk_sp + // sk_ref_sp increments ref count before passing + return ToImage(AsImage(image)->makeWithFilter( + sk_ref_sp(AsImageFilter(filter))).release()); +} +``` + +#### Boolean Return for Success/Failure + +```cpp +SK_C_API bool sk_bitmap_try_alloc_pixels( + sk_bitmap_t* bitmap, + const sk_imageinfo_t* info) +{ + // C++ method naturally returns bool + return AsBitmap(bitmap)->tryAllocPixels(AsImageInfo(info)); +} +``` + +**Note:** C# validates `bitmap` and `info` before calling. + +#### Null Return for Factory Failure + +```cpp +SK_C_API sk_surface_t* sk_surface_new_raster(const sk_imageinfo_t* info) { + auto surface = SkSurfaces::Raster(AsImageInfo(info)); + return ToSurface(surface.release()); // Returns nullptr if Skia factory fails +} +``` + +**Note:** C# checks for `IntPtr.Zero` and throws exception. + +## Step 3: Add P/Invoke Declaration + +### Manual Declaration + +For simple functions, add to `binding/SkiaSharp/SkiaApi.cs`: + +```csharp +// In SkiaApi.cs +internal static partial class SkiaApi +{ + [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] + public static extern void sk_canvas_draw_arc( + sk_canvas_t canvas, + sk_rect_t* oval, + float startAngle, + float sweepAngle, + [MarshalAs(UnmanagedType.I1)] bool useCenter, + sk_paint_t paint); +} +``` + +**Type mappings:** +- C `sk_canvas_t*` → C# `sk_canvas_t` (IntPtr alias) +- C `const sk_rect_t*` → C# `sk_rect_t*` (pointer) +- C `float` → C# `float` +- C `bool` → C# `bool` with `MarshalAs(UnmanagedType.I1)` +- C `sk_paint_t*` → C# `sk_paint_t` (IntPtr alias) + +**Note:** Bool marshaling ensures correct size (1 byte). + +### Generated Declaration + +For bulk additions, update generator config and regenerate: + +```bash +dotnet run --project utils/SkiaSharpGenerator/SkiaSharpGenerator.csproj -- generate +``` + +The generator creates `SkiaApi.generated.cs` with P/Invoke declarations. + +## Step 4: Add C# Wrapper + +### Determine Wrapper Pattern + +Based on the pointer type analysis: + +| Pointer Type | C# Pattern | Example | +|--------------|------------|---------| +| Owned | `SKObject` with `DisposeNative()` | `SKCanvas`, `SKPaint` | +| Reference-Counted | `SKObject, ISKReferenceCounted` | `SKImage`, `SKShader` | +| Non-Owning | `OwnsHandle = false` | Returned child objects | + +### Add Method to C# Class + +```csharp +// In binding/SkiaSharp/SKCanvas.cs + +public unsafe class SKCanvas : SKObject +{ + public void DrawArc(SKRect oval, float startAngle, float sweepAngle, bool useCenter, SKPaint paint) + { + // Step 1: Validate parameters + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + + // Step 2: Call P/Invoke + SkiaApi.sk_canvas_draw_arc(Handle, &oval, startAngle, sweepAngle, useCenter, paint.Handle); + } +} +``` + +**Best practices:** +1. **Parameter validation** - Check nulls, disposed objects +2. **Proper marshaling** - Use `&` for struct pointers +3. **Resource tracking** - Handle ownership transfers if needed +4. **Documentation** - Add XML comments + +### Handle Different Return Types + +#### Void Return (No Error) + +```csharp +public void DrawArc(SKRect oval, float startAngle, float sweepAngle, bool useCenter, SKPaint paint) +{ + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + + SkiaApi.sk_canvas_draw_arc(Handle, &oval, startAngle, sweepAngle, useCenter, paint.Handle); +} +``` + +#### Boolean Return (Success/Failure) + +```csharp +public bool TryAllocPixels(SKImageInfo info) +{ + var nInfo = SKImageInfoNative.FromManaged(ref info); + return SkiaApi.sk_bitmap_try_alloc_pixels(Handle, &nInfo); +} + +// Or throw on failure: +public void AllocPixels(SKImageInfo info) +{ + if (!TryAllocPixels(info)) + throw new InvalidOperationException($"Failed to allocate {info.Width}x{info.Height} pixels"); +} +``` + +#### Owned Pointer Return + +```csharp +public static SKCanvas Create(SKBitmap bitmap) +{ + if (bitmap == null) + throw new ArgumentNullException(nameof(bitmap)); + + var handle = SkiaApi.sk_canvas_new_from_bitmap(bitmap.Handle); + + if (handle == IntPtr.Zero) + throw new InvalidOperationException("Failed to create canvas"); + + // Returns owned object + return GetOrAddObject(handle, owns: true, (h, o) => new SKCanvas(h, o)); +} +``` + +#### Reference-Counted Pointer Return + +```csharp +public static SKImage FromBitmap(SKBitmap bitmap) +{ + if (bitmap == null) + throw new ArgumentNullException(nameof(bitmap)); + + var handle = SkiaApi.sk_image_new_from_bitmap(bitmap.Handle); + + // Returns ref-counted object (ref count = 1), or null if failed + return GetObject(handle); +} + +// ✅ Usage - check for null +var image = SKImage.FromBitmap(bitmap); +if (image == null) + throw new InvalidOperationException("Failed to create image"); +``` + +#### Non-Owning Pointer Return + +```csharp +public SKSurface Surface +{ + get { + var handle = SkiaApi.sk_get_surface(Handle); + if (handle == IntPtr.Zero) + return null; + + // Surface owned by canvas, return non-owning wrapper + return GetOrAddObject(handle, owns: false, (h, o) => new SKSurface(h, o)); + } +} +``` + +### Handle Ownership Transfer + +```csharp +public void DrawDrawable(SKDrawable drawable, SKMatrix matrix) +{ + if (drawable == null) + throw new ArgumentNullException(nameof(drawable)); + + // Canvas takes ownership of drawable + drawable.RevokeOwnership(this); + + SkiaApi.sk_canvas_draw_drawable(Handle, drawable.Handle, &matrix); +} +``` + +### Add Overloads for Convenience + +```csharp +// Core method with all parameters +public void DrawArc(SKRect oval, float startAngle, float sweepAngle, bool useCenter, SKPaint paint) +{ + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + SkiaApi.sk_canvas_draw_arc(Handle, &oval, startAngle, sweepAngle, useCenter, paint.Handle); +} + +// Overload with SKPoint center and radius +public void DrawArc(SKPoint center, float radius, float startAngle, float sweepAngle, bool useCenter, SKPaint paint) +{ + var oval = new SKRect( + center.X - radius, center.Y - radius, + center.X + radius, center.Y + radius); + DrawArc(oval, startAngle, sweepAngle, useCenter, paint); +} + +// Overload with individual coordinates +public void DrawArc(float left, float top, float right, float bottom, + float startAngle, float sweepAngle, bool useCenter, SKPaint paint) +{ + DrawArc(new SKRect(left, top, right, bottom), startAngle, sweepAngle, useCenter, paint); +} +``` + +## Complete Example: Adding DrawArc + +### C API Header + +```cpp +// externals/skia/include/c/sk_canvas.h +SK_C_API void sk_canvas_draw_arc( + sk_canvas_t* ccanvas, + const sk_rect_t* oval, + float startAngle, + float sweepAngle, + bool useCenter, + const sk_paint_t* cpaint); +``` + +### C API Implementation + +```cpp +// externals/skia/src/c/sk_canvas.cpp +void sk_canvas_draw_arc( + sk_canvas_t* ccanvas, + const sk_rect_t* oval, + float startAngle, + float sweepAngle, + bool useCenter, + const sk_paint_t* cpaint) +{ + if (!ccanvas || !oval || !cpaint) + return; + + AsCanvas(ccanvas)->drawArc(*AsRect(oval), startAngle, sweepAngle, useCenter, *AsPaint(cpaint)); +} +``` + +### P/Invoke Declaration + +```csharp +// binding/SkiaSharp/SkiaApi.cs +[DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] +public static extern void sk_canvas_draw_arc( + sk_canvas_t canvas, + sk_rect_t* oval, + float startAngle, + float sweepAngle, + [MarshalAs(UnmanagedType.I1)] bool useCenter, + sk_paint_t paint); +``` + +### C# Wrapper + +```csharp +// binding/SkiaSharp/SKCanvas.cs +public unsafe class SKCanvas : SKObject +{ + /// + /// Draws an arc of an oval. + /// + /// Bounds of oval containing arc. + /// Starting angle in degrees. + /// Sweep angle in degrees; positive is clockwise. + /// If true, include the center of the oval. + /// Paint to use for the arc. + public void DrawArc(SKRect oval, float startAngle, float sweepAngle, bool useCenter, SKPaint paint) + { + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + + SkiaApi.sk_canvas_draw_arc(Handle, &oval, startAngle, sweepAngle, useCenter, paint.Handle); + } +} +``` + +## Testing Your Changes + +### Build and Test + +```bash +# Build the project +dotnet cake --target=libs + +# Run tests +dotnet cake --target=tests +``` + +### Write Unit Tests + +```csharp +// tests/SkiaSharp.Tests/SKCanvasTest.cs +[Fact] +public void DrawArcRendersCorrectly() +{ + using (var bitmap = new SKBitmap(100, 100)) + using (var canvas = new SKCanvas(bitmap)) + using (var paint = new SKPaint { Color = SKColors.Red, Style = SKPaintStyle.Stroke, StrokeWidth = 2 }) + { + canvas.Clear(SKColors.White); + canvas.DrawArc(new SKRect(10, 10, 90, 90), 0, 90, false, paint); + + // Verify arc was drawn + Assert.NotEqual(SKColors.White, bitmap.GetPixel(50, 10)); + } +} + +[Fact] +public void DrawArcThrowsOnNullPaint() +{ + using (var bitmap = new SKBitmap(100, 100)) + using (var canvas = new SKCanvas(bitmap)) + { + Assert.Throws(() => + canvas.DrawArc(new SKRect(10, 10, 90, 90), 0, 90, false, null)); + } +} +``` + +### Manual Testing + +```csharp +using SkiaSharp; + +using (var bitmap = new SKBitmap(400, 400)) +using (var canvas = new SKCanvas(bitmap)) +using (var paint = new SKPaint { Color = SKColors.Blue, Style = SKPaintStyle.Stroke, StrokeWidth = 4 }) +{ + canvas.Clear(SKColors.White); + + // Test various arcs + canvas.DrawArc(new SKRect(50, 50, 150, 150), 0, 90, false, paint); // Open arc + canvas.DrawArc(new SKRect(200, 50, 300, 150), 0, 90, true, paint); // Closed arc + canvas.DrawArc(new SKRect(50, 200, 150, 300), -45, 180, false, paint); // Larger sweep + + // Save to file + using (var image = SKImage.FromBitmap(bitmap)) + using (var data = image.Encode(SKEncodedImageFormat.Png, 100)) + using (var stream = File.OpenWrite("arc_test.png")) + { + data.SaveTo(stream); + } +} +``` + +## Common Patterns and Examples + +### Pattern: Simple Drawing Method + +**C++:** `void SkCanvas::drawCircle(SkPoint center, SkScalar radius, const SkPaint& paint)` + +```cpp +// C API +SK_C_API void sk_canvas_draw_circle(sk_canvas_t* canvas, float cx, float cy, float radius, const sk_paint_t* paint); + +void sk_canvas_draw_circle(sk_canvas_t* canvas, float cx, float cy, float radius, const sk_paint_t* paint) { + AsCanvas(canvas)->drawCircle(cx, cy, radius, *AsPaint(paint)); +} +``` + +```csharp +// C# +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); +} + +public void DrawCircle(SKPoint center, float radius, SKPaint paint) => + DrawCircle(center.X, center.Y, radius, paint); +``` + +### Pattern: Factory with Reference Counting + +**C++:** `sk_sp SkImages::DeferredFromEncodedData(sk_sp data)` + +```cpp +// C API +SK_C_API sk_image_t* sk_image_new_from_encoded(const sk_data_t* data); + +sk_image_t* sk_image_new_from_encoded(const sk_data_t* data) { + return ToImage(SkImages::DeferredFromEncodedData(sk_ref_sp(AsData(data))).release()); +} +``` + +```csharp +// C# +public static SKImage FromEncodedData(SKData data) +{ + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var handle = SkiaApi.sk_image_new_from_encoded(data.Handle); + if (handle == IntPtr.Zero) + throw new InvalidOperationException("Failed to decode image"); + + return GetObject(handle); +} +``` + +### Pattern: Mutable Object Creation + +**C++:** `SkPaint::SkPaint()` + +```cpp +// C API +SK_C_API sk_paint_t* sk_paint_new(void); +SK_C_API void sk_paint_delete(sk_paint_t* paint); + +sk_paint_t* sk_paint_new(void) { + return ToPaint(new SkPaint()); +} + +void sk_paint_delete(sk_paint_t* paint) { + delete AsPaint(paint); +} +``` + +```csharp +// C# +public class SKPaint : SKObject, ISKSkipObjectRegistration +{ + public SKPaint() : base(IntPtr.Zero, true) + { + Handle = SkiaApi.sk_paint_new(); + } + + protected override void DisposeNative() + { + SkiaApi.sk_paint_delete(Handle); + } +} +``` + +### Pattern: Property Getter/Setter + +**C++:** `SkColor SkPaint::getColor()` and `void SkPaint::setColor(SkColor color)` + +```cpp +// C API +SK_C_API sk_color_t sk_paint_get_color(const sk_paint_t* paint); +SK_C_API void sk_paint_set_color(sk_paint_t* paint, sk_color_t color); + +sk_color_t sk_paint_get_color(const sk_paint_t* paint) { + return AsPaint(paint)->getColor(); +} + +void sk_paint_set_color(sk_paint_t* paint, sk_color_t color) { + AsPaint(paint)->setColor(color); +} +``` + +```csharp +// C# +public SKColor Color +{ + get => (SKColor)SkiaApi.sk_paint_get_color(Handle); + set => SkiaApi.sk_paint_set_color(Handle, (uint)value); +} +``` + +## Checklist for Adding New APIs + +### Analysis Phase +- [ ] Located C++ API in Skia headers +- [ ] Identified pointer type (raw, owned, ref-counted) +- [ ] Determined ownership semantics +- [ ] Checked error conditions +- [ ] Read Skia documentation/comments + +### C API Layer +- [ ] Added function declaration to header +- [ ] Implemented function in .cpp file +- [ ] Used correct type conversion macros +- [ ] Handled ref-counting correctly (if applicable) +- [ ] Used `.release()` on `sk_sp` returns +- [ ] Used `sk_ref_sp()` for ref-counted parameters + +### P/Invoke Layer +- [ ] Added P/Invoke declaration +- [ ] Used correct type mappings +- [ ] Applied correct marshaling attributes +- [ ] Specified calling convention + +### C# Wrapper Layer +- [ ] Added method to appropriate class +- [ ] Validated parameters +- [ ] Checked return values +- [ ] Handled ownership correctly +- [ ] Added XML documentation +- [ ] Created convenience overloads + +### Testing +- [ ] Built project successfully +- [ ] Wrote unit tests +- [ ] Manual testing completed +- [ ] Verified memory management (no leaks) +- [ ] Tested error cases + +## Troubleshooting + +### Common Build Errors + +**"Cannot find sk_canvas_draw_arc"** +- C API function not exported from native library +- Rebuild native library: `dotnet cake --target=externals` + +**"Method not found" at runtime** +- P/Invoke signature doesn't match C API +- Check calling convention and parameter types + +**Memory leaks** +- Check pointer type identification +- Verify ownership transfer +- Use memory profiler to track leaks + +### Common Runtime Errors + +**Crash in native code** +- Null pointer passed to C API +- Add null checks in C API layer +- Add validation in C# layer + +**ObjectDisposedException** +- Using disposed object +- Check object lifecycle +- Don't cache references to child objects + +**InvalidOperationException** +- C API returned error +- Check return value handling +- Verify error conditions + +## Next Steps + +- Review [Architecture Overview](architecture-overview.md) for context +- Study [Memory Management](memory-management.md) for pointer types +- Read [Error Handling](error-handling.md) for error patterns +- See [Layer Mapping](layer-mapping.md) for detailed type mappings + +## Additional Resources + +- Existing wiki: [Creating Bindings](https://github.com/mono/SkiaSharp/wiki/Creating-Bindings) +- Skia C++ documentation: https://skia.org/docs/ +- Example PRs adding new APIs in SkiaSharp repository diff --git a/design/architecture-overview.md b/design/architecture-overview.md new file mode 100644 index 0000000000..b3ba9dbcc2 --- /dev/null +++ b/design/architecture-overview.md @@ -0,0 +1,649 @@ +# SkiaSharp Architecture Overview + +> **Quick Start:** For a practical tutorial, see [QUICKSTART.md](QUICKSTART.md) +> **Quick Reference:** For a 2-minute overview, see [AGENTS.md](../AGENTS.md) + +## TL;DR + +**Three-layer architecture bridges C++ to C#:** + +1. **C# Wrapper Layer** (`binding/SkiaSharp/`) + - Public .NET API (SKCanvas, SKPaint, etc.) + - Validates parameters, throws exceptions + - Manages object lifecycles with IDisposable + +2. **C API Layer** (`externals/skia/src/c/`) + - C functions as P/Invoke targets + - **Minimal wrapper** - trusts C# validation + - Returns error codes (bool/null), never throws + +3. **C++ Skia Layer** (`externals/skia/`) + - Native graphics library + - Can throw exceptions (C++ code) + +**Call flow:** `SKCanvas.DrawRect()` → (P/Invoke) → `sk_canvas_draw_rect()` → (type cast) → `SkCanvas::drawRect()` + +**Key design principles:** +- C# is the safety boundary - validates all inputs +- C API is minimal wrapper - no validation needed +- Each layer has distinct responsibilities +- Type conversions happen at layer boundaries + +See sections below for details on each layer, threading, and code generation. + +--- + +## Introduction + +SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library. It provides a three-layer architecture that wraps the native C++ Skia library in a safe, idiomatic C# API. + +## Three-Layer Architecture + +SkiaSharp's architecture consists of three distinct layers that work together to provide C# access to native Skia functionality: + +```mermaid +graph TB + subgraph Layer1["C# Wrapper Layer
(binding/SkiaSharp/*.cs)"] + L1A[SKCanvas, SKPaint, SKImage, etc.] + L1B[Object-oriented C# API] + L1C[Memory management & lifecycle] + L1D[Type safety & null checking] + end + + subgraph Layer2["C API Layer
(externals/skia/include/c/*.h
externals/skia/src/c/*.cpp)"] + L2A[sk_canvas_*, sk_paint_*, sk_image_*, etc.] + L2B[C functions with SK_C_API] + L2C[Minimal wrapper - trusts C# validation] + L2D[Return error codes bool/nullptr] + end + + subgraph Layer3["C++ Skia Layer
(externals/skia/)"] + L3A[SkCanvas, SkPaint, SkImage, etc.] + L3B[Native Skia graphics library] + L3C[Full C++ API with exceptions] + L3D[sk_sp smart pointers] + end + + Layer1 -->|P/Invoke
SkiaApi.cs| Layer2 + Layer2 -->|Type conversion
AsCanvas/ToCanvas| Layer3 + + style Layer1 fill:#e1f5e1 + style Layer2 fill:#fff4e1 + style Layer3 fill:#e1e8f5 +``` + +### Layer 1: C++ Skia Library (Native) + +**Location:** `externals/skia/include/core/` and `externals/skia/src/` + +The bottom layer is Google's Skia Graphics Library written in C++. This is the actual graphics engine that performs all rendering operations. + +**Key characteristics:** +- Object-oriented C++ API +- Uses C++ features: classes, inheritance, templates, smart pointers +- Memory management via destructors, RAII, and reference counting +- Exception handling via C++ exceptions +- Cannot be directly called from C# (different ABIs) + +**Example types:** +- `SkCanvas` - Drawing surface +- `SkPaint` - Drawing attributes (owned resource) +- `SkImage` - Immutable image (reference counted via `sk_sp`) + +### Layer 2: C API Layer (Bridge) + +**Location:** `externals/skia/include/c/*.h` and `externals/skia/src/c/*.cpp` + +The middle layer is a hand-written C API that wraps the C++ API. This layer is essential because: +- C has a stable ABI that can be P/Invoked from C# +- C functions can cross the managed/unmanaged boundary +- C++ exceptions cannot cross this boundary safely + +**Key characteristics:** +- Pure C functions (no classes or exceptions) +- Opaque pointer types (`sk_canvas_t*`, `sk_paint_t*`, `sk_image_t*`) +- Manual resource management (create/destroy functions) +- Type conversion macros to cast between C and C++ types +- **Minimal wrapper** - no validation, trusts C# layer + +**Naming convention:** +- C API headers: `sk_.h` (e.g., `sk_canvas.h`) +- C API implementations: `sk_.cpp` (e.g., `sk_canvas.cpp`) +- C API functions: `sk__` (e.g., `sk_canvas_draw_rect`) +- C API types: `sk__t` (e.g., `sk_canvas_t*`) + +**Type conversion macros:** +```cpp +// In sk_types_priv.h +DEF_CLASS_MAP(SkCanvas, sk_canvas_t, Canvas) +// Generates: +// AsCanvas(sk_canvas_t*) -> SkCanvas* +// ToCanvas(SkCanvas*) -> sk_canvas_t* +``` + +**Example:** +```cpp +// C API in sk_canvas.h +SK_C_API void sk_canvas_draw_rect(sk_canvas_t* ccanvas, const sk_rect_t* crect, const sk_paint_t* cpaint); + +// Implementation in sk_canvas.cpp +void sk_canvas_draw_rect(sk_canvas_t* ccanvas, const sk_rect_t* crect, const sk_paint_t* cpaint) { + AsCanvas(ccanvas)->drawRect(*AsRect(crect), *AsPaint(cpaint)); +} +``` + +### Layer 3: C# Wrapper Layer (Managed) + +**Location:** `binding/SkiaSharp/*.cs` + +The top layer is a C# object-oriented wrapper that provides: +- Idiomatic C# API matching Skia's C++ API style +- Automatic memory management via `IDisposable` +- Type safety and null checking +- Properties instead of get/set methods +- .NET naming conventions + +**Key characteristics:** +- Object-oriented classes (SKCanvas, SKPaint, SKImage) +- P/Invoke declarations in `SkiaApi.cs` and `SkiaApi.generated.cs` +- Base class `SKObject` handles lifecycle and disposal +- Handle-based tracking via `IntPtr` to native resources +- Global handle dictionary for object identity + +**Base class hierarchy:** +``` +SKNativeObject (IDisposable) + └─ SKObject (adds handle dictionary & ref counting) + ├─ SKCanvas (owned resource, destroy on dispose) + ├─ SKPaint (owned resource, delete on dispose) + ├─ SKImage (reference counted, unref on dispose) + └─ ... +``` + +**Example:** +```csharp +// C# API in SKCanvas.cs +public class SKCanvas : SKObject +{ + public void DrawRect(SKRect rect, SKPaint paint) + { + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); + } +} +``` + +## Call Flow Example: DrawRect + +Here's how a single method call flows through all three layers: + +```csharp +// 1. C# Application Code +var canvas = surface.Canvas; +var paint = new SKPaint { Color = SKColors.Red }; +canvas.DrawRect(new SKRect(10, 10, 100, 100), paint); + +// 2. C# Wrapper (SKCanvas.cs) +public void DrawRect(SKRect rect, SKPaint paint) +{ + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); +} + +// 3. P/Invoke Declaration (SkiaApi.cs) +[DllImport("libSkiaSharp")] +public static extern void sk_canvas_draw_rect( + sk_canvas_t canvas, + sk_rect_t* rect, + sk_paint_t paint); + +// 4. C API Implementation (sk_canvas.cpp) +void sk_canvas_draw_rect(sk_canvas_t* ccanvas, const sk_rect_t* crect, const sk_paint_t* cpaint) { + AsCanvas(ccanvas)->drawRect(*AsRect(crect), *AsPaint(cpaint)); +} + +// 5. C++ Skia (SkCanvas.h/cpp) +void SkCanvas::drawRect(const SkRect& rect, const SkPaint& paint) { + // Native implementation performs actual rendering +} +``` + +## File Organization + +### C++ Layer (Skia Native) +``` +externals/skia/ +├── include/core/ # C++ public headers +│ ├── SkCanvas.h +│ ├── SkPaint.h +│ ├── SkImage.h +│ └── ... +└── src/ # C++ implementation (internal) +``` + +### C API Layer (Bridge) +``` +externals/skia/ +├── include/c/ # C API headers (public) +│ ├── sk_canvas.h +│ ├── sk_paint.h +│ ├── sk_image.h +│ ├── sk_types.h # Common type definitions +│ └── ... +└── src/c/ # C API implementation + ├── sk_canvas.cpp + ├── sk_paint.cpp + ├── sk_image.cpp + ├── sk_types_priv.h # Type conversion macros + └── ... +``` + +### C# Wrapper Layer +``` +binding/SkiaSharp/ +├── SKCanvas.cs # C# wrapper for sk_canvas_t +├── SKPaint.cs # C# wrapper for sk_paint_t +├── SKImage.cs # C# wrapper for sk_image_t +├── SKObject.cs # Base class for all wrappers +├── SkiaApi.cs # Manual P/Invoke declarations +├── SkiaApi.generated.cs # Generated P/Invoke declarations +└── ... +``` + +## Naming Conventions + +### Mapping Between Layers + +The naming follows consistent patterns across layers: + +| C++ Class | C API Header | C API Type | C API Functions | C# Class | +|-----------|--------------|------------|-----------------|----------| +| `SkCanvas` | `sk_canvas.h` | `sk_canvas_t*` | `sk_canvas_*` | `SKCanvas` | +| `SkPaint` | `sk_paint.h` | `sk_paint_t*` | `sk_paint_*` | `SKPaint` | +| `SkImage` | `sk_image.h` | `sk_image_t*` | `sk_image_*` | `SKImage` | + +### Function Naming Patterns + +**C API functions follow the pattern:** `sk__[_
]` + +Examples: +- `sk_canvas_draw_rect` - Draw a rectangle on canvas +- `sk_paint_set_color` - Set paint color +- `sk_image_new_from_bitmap` - Create image from bitmap +- `sk_canvas_save_layer` - Save canvas layer + +## Code Generation + +SkiaSharp uses a combination of hand-written and generated code: + +### Hand-Written Code +- **C API layer**: All C wrapper functions in `externals/skia/src/c/*.cpp` +- **C# wrapper logic**: Core classes and complex logic in `binding/SkiaSharp/*.cs` +- **P/Invoke declarations**: Some manual declarations in `SkiaApi.cs` + +### Generated Code +- **P/Invoke declarations**: `SkiaApi.generated.cs` contains generated P/Invoke signatures +- **Generation tool**: `utils/SkiaSharpGenerator/` contains the code generator +- **Configuration**: Type mappings and function mappings in `utils/SkiaSharpGenerator/ConfigJson/` + +The generator parses C header files and creates: +1. P/Invoke declarations with correct signatures +2. Type aliases and constants +3. Enum definitions + +**To regenerate code:** +```bash +dotnet run --project utils/SkiaSharpGenerator/SkiaSharpGenerator.csproj -- generate --config +``` + +## Key Design Principles + +### 1. Handle-Based Pattern +- Native objects are represented as `IntPtr` handles in C# +- Handles are opaque pointers to native memory +- C# objects wrap handles and manage their lifecycle + +### 2. Object Identity +- Global `HandleDictionary` ensures only one C# wrapper per native handle +- Prevents duplicate wrappers and ensures reference equality +- Critical for reference-counted objects + +### 3. Memory Safety +- C# wrappers implement `IDisposable` for deterministic cleanup +- Finalizers provide backup cleanup if `Dispose()` not called +- `OwnsHandle` flag determines disposal responsibility + +### 4. Exception Boundaries +- C++ exceptions cannot cross C API boundary +- C API functions never throw; use return values for errors +- C# layer performs validation and throws appropriate exceptions + +### 5. Minimal P/Invoke Overhead +- Direct handle passing (no marshaling when possible) +- Struct parameters passed by pointer +- Bulk operations to minimize transitions + +## Threading Model and Concurrency + +### TL;DR - Thread Safety Rules + +**⚠️ Skia is NOT thread-safe by default** + +| Object Type | Thread Safety | Can Share? | Notes | +|-------------|---------------|------------|-------| +| **Canvas** | ❌ Not thread-safe | No | Single-threaded drawing only | +| **Paint** | ❌ Not thread-safe | No | Each thread needs own instance | +| **Path** | ❌ Not thread-safe | No | Build paths on one thread | +| **Bitmap (mutable)** | ❌ Not thread-safe | No | Modifications single-threaded | +| **Image (immutable)** | ✅ Read-only safe | Yes | Once created, can share | +| **Shader** | ✅ Read-only safe | Yes | Immutable, can share | +| **Typeface** | ✅ Read-only safe | Yes | Immutable, can share | +| **Data** | ✅ Read-only safe | Yes | Immutable, can share | +| **Creation** | ✅ Usually safe | N/A | Creating objects on different threads OK | + +**Golden Rule:** Don't use the same SKCanvas/SKPaint/SKPath from multiple threads simultaneously. + +### Detailed Threading Behavior + +#### Mutable Objects (Not Thread-Safe) + +**Examples:** SKCanvas, SKPaint, SKPath, SKBitmap (when being modified) + +**Behavior:** +- Single-threaded use only +- No internal locking +- Race conditions if accessed from multiple threads +- Undefined behavior if concurrent access + +**Correct Pattern:** +```csharp +// ✅ GOOD: Each thread has its own canvas +void DrawOnThread1() +{ + using var surface1 = SKSurface.Create(info); + using var canvas1 = surface1.Canvas; + canvas1.DrawRect(...); // Thread 1 only +} + +void DrawOnThread2() +{ + using var surface2 = SKSurface.Create(info); + using var canvas2 = surface2.Canvas; + canvas2.DrawRect(...); // Thread 2 only +} +``` + +**Wrong Pattern:** +```csharp +// ❌ BAD: Sharing canvas between threads +SKCanvas sharedCanvas; + +void DrawOnThread1() +{ + sharedCanvas.DrawRect(...); // RACE CONDITION! +} + +void DrawOnThread2() +{ + sharedCanvas.DrawCircle(...); // RACE CONDITION! +} +``` + +#### Immutable Objects (Thread-Safe for Reading) + +**Examples:** SKImage, SKShader, SKTypeface, SKData, SKPicture + +**Behavior:** +- Read-only after creation +- Safe to share across threads +- Reference counting is thread-safe (atomic operations) +- Multiple threads can use the same instance concurrently + +**Pattern:** +```csharp +// ✅ GOOD: Share immutable image across threads +SKImage sharedImage = SKImage.FromEncodedData(data); + +void DrawOnThread1() +{ + using var surface = SKSurface.Create(info); + surface.Canvas.DrawImage(sharedImage, 0, 0); // Safe +} + +void DrawOnThread2() +{ + using var surface = SKSurface.Create(info); + surface.Canvas.DrawImage(sharedImage, 0, 0); // Safe (same image) +} +``` + +#### Object Creation (Usually Thread-Safe) + +Creating different objects on different threads is generally safe: + +```csharp +// ✅ GOOD: Create different objects on different threads +var task1 = Task.Run(() => +{ + using var paint1 = new SKPaint { Color = SKColors.Red }; + using var surface1 = SKSurface.Create(info); + // ... draw with paint1 and surface1 +}); + +var task2 = Task.Run(() => +{ + using var paint2 = new SKPaint { Color = SKColors.Blue }; + using var surface2 = SKSurface.Create(info); + // ... draw with paint2 and surface2 +}); + +await Task.WhenAll(task1, task2); // Safe - different objects +``` + +### Threading Visualization + +```mermaid +graph TB + subgraph Thread1["Thread 1"] + T1Canvas[SKCanvas 1] + T1Paint[SKPaint 1] + T1Path[SKPath 1] + end + + subgraph Thread2["Thread 2"] + T2Canvas[SKCanvas 2] + T2Paint[SKPaint 2] + T2Path[SKPath 2] + end + + subgraph Shared["Shared (Immutable)"] + Image[SKImage] + Shader[SKShader] + Typeface[SKTypeface] + end + + T1Canvas -->|Uses| Image + T2Canvas -->|Uses| Image + T1Paint -->|Uses| Shader + T2Paint -->|Uses| Shader + + style T1Canvas fill:#ffe1e1 + style T1Paint fill:#ffe1e1 + style T1Path fill:#ffe1e1 + style T2Canvas fill:#ffe1e1 + style T2Paint fill:#ffe1e1 + style T2Path fill:#ffe1e1 + style Image fill:#e1f5e1 + style Shader fill:#e1f5e1 + style Typeface fill:#e1f5e1 +``` + +**Legend:** +- 🔴 Red (mutable) = Thread-local only +- 🟢 Green (immutable) = Can be shared + +### C# Wrapper Thread Safety + +**HandleDictionary:** +- Backed by a `Dictionary` protected by a reader/writer lock (`IPlatformLock`) +- Uses `EnterReadLock` for lookups and `EnterUpgradeableReadLock` for add operations +- Prevents duplicate wrappers for the same native handle + +**Reference Counting:** +- `ref()` and `unref()` operations are thread-safe +- Uses atomic operations in native Skia +- Safe to dispose from different thread than creation + +**Disposal:** +- `Dispose()` can be called from any thread +- But object must not be in use on another thread +- Finalizer may run on GC thread + +### Common Threading Scenarios + +#### Scenario 1: Background Rendering + +```csharp +// ✅ GOOD: Render on background thread +var image = await Task.Run(() => +{ + var info = new SKImageInfo(width, height); + using var surface = SKSurface.Create(info); + using var canvas = surface.Canvas; + using var paint = new SKPaint(); + + // All objects local to this thread + canvas.Clear(SKColors.White); + canvas.DrawCircle(100, 100, 50, paint); + + return surface.Snapshot(); // Returns immutable SKImage +}); + +// Use image on UI thread +imageView.Image = image; +``` + +#### Scenario 2: Parallel Tile Rendering + +```csharp +// ✅ GOOD: Render tiles in parallel +var tiles = Enumerable.Range(0, tileCount).Select(i => + Task.Run(() => RenderTile(i)) +).ToArray(); + +await Task.WhenAll(tiles); + +SKImage RenderTile(int index) +{ + // Each task has its own objects + using var surface = SKSurface.Create(tileInfo); + using var canvas = surface.Canvas; + using var paint = new SKPaint(); + // ... render tile + return surface.Snapshot(); +} +``` + +#### Scenario 3: Shared Resources + +```csharp +// ✅ GOOD: Load shared resources once +class GraphicsCache +{ + private static readonly SKTypeface _font = SKTypeface.FromFile("font.ttf"); + private static readonly SKImage _logo = SKImage.FromEncodedData("logo.png"); + + // Multiple threads can use these immutable objects + public static void DrawWithSharedResources(SKCanvas canvas) + { + using var paint = new SKPaint { Typeface = _font }; + canvas.DrawText("Hello", 0, 0, paint); + canvas.DrawImage(_logo, 100, 100); + } +} +``` + +### Platform-Specific Threading Considerations + +#### UI Thread Affinity + +Some platforms require graphics operations on specific threads: + +**❌ Android/iOS:** Some surface operations may require UI thread +**✅ Offscreen rendering:** Usually safe on any thread +**✅ Image decoding:** Safe on background threads + +```csharp +// Example: Decode on background, display on UI +var bitmap = await Task.Run(() => +{ + // Decode on background thread + return SKBitmap.Decode("large-image.jpg"); +}); + +// Use on UI thread +await MainThread.InvokeOnMainThreadAsync(() => +{ + imageView.Bitmap = bitmap; +}); +``` + +### Debugging Threading Issues + +**Symptoms of threading bugs:** +- Crashes with no clear cause +- Corrupted rendering +- Access violations +- Intermittent failures + +**Tools to help:** +```csharp +// Add thread ID assertions in debug builds +#if DEBUG +private readonly int _creationThread = Thread.CurrentThread.ManagedThreadId; + +private void AssertCorrectThread() +{ + if (Thread.CurrentThread.ManagedThreadId != _creationThread) + throw new InvalidOperationException("Cross-thread access detected!"); +} +#endif +``` + +### Best Practices Summary + +1. **✅ DO:** Keep mutable objects (Canvas, Paint, Path) thread-local +2. **✅ DO:** Share immutable objects (Image, Shader, Typeface) freely +3. **✅ DO:** Create objects on background threads for offscreen rendering +4. **✅ DO:** Use thread-local storage or task-based parallelism +5. **❌ DON'T:** Share SKCanvas across threads +6. **❌ DON'T:** Modify SKPaint while another thread uses it +7. **❌ DON'T:** Assume automatic synchronization +8. **❌ DON'T:** Dispose objects still in use on other threads + +### SkiaSharp Threading Architecture + +**No automatic locking:** +- SkiaSharp wrappers don't add locks +- Performance-critical design +- Developer responsibility to ensure thread safety + +**Thread-safe components:** +- `HandleDictionary` serializes access with a reader/writer lock around a shared dictionary +- Reference counting uses atomic operations +- Disposal is thread-safe (but must not be in use) + +**Not thread-safe:** +- Individual wrapper objects (SKCanvas, SKPaint, etc.) +- Mutable operations +- State changes + +## Next Steps + +For more detailed information, see: +- [Memory Management](memory-management.md) - Pointer types, ownership, and lifecycle +- [Error Handling](error-handling.md) - How errors propagate through layers +- [Adding New APIs](adding-new-apis.md) - Step-by-step guide for contributors +- [Layer Mapping](layer-mapping.md) - Detailed layer-to-layer mappings diff --git a/design/error-handling.md b/design/error-handling.md new file mode 100644 index 0000000000..b2c62522aa --- /dev/null +++ b/design/error-handling.md @@ -0,0 +1,740 @@ +# Error Handling in SkiaSharp + +> **Quick Start:** For a practical tutorial, see [QUICKSTART.md](QUICKSTART.md) +> **Quick Reference:** For a 2-minute overview, see [AGENTS.md](../AGENTS.md) + +## TL;DR + +**Safety boundary highlights:** + +- **C++ Layer:** Native Skia code can throw; we do not try to surface those exceptions directly. +- **C API Layer:** Thin pass-through functions. They rarely guard inputs and never wrap calls in `try/catch`; they simply forward Skia's return values (void/bool/pointer). +- **C# Layer:** Performs targeted validation where it is required, but behaviour differs by API: constructors usually throw on failure, while many factory/utility methods return `null`, `false`, or default values instead of throwing. + +**C# error patterns you will see:** +1. **Null parameter guards** – most methods throw `ArgumentNullException` before calling into native code, e.g. `SKCanvas.DrawRect` checks `paint`. +2. **Constructor validation** – constructors check if native handle creation succeeded and throw `InvalidOperationException` if Handle is IntPtr.Zero. +3. **Return value propagation** – factory methods such as `SKImage.FromEncodedData` simply return `null` and expect the caller to inspect the result. +4. **Try methods** – methods like `SKBitmap.TryAllocPixels` return `false` on failure rather than throwing. + +**Key principle:** The managed layer is the safety boundary, but it mixes throwing and non-throwing patterns. Document both behaviours so callers know whether to check return values or catch exceptions. + +**Representative code in the repo today:** +```csharp +public void DrawRect(SKRect rect, SKPaint paint) +{ + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); +} + +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 when decode fails +} +``` + +```cpp +// C API forwards directly to Skia – no exception handling and minimal validation. +SK_C_API 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)); +} +``` + +--- + +## Introduction + +Error handling in SkiaSharp must navigate the complexities of crossing managed/unmanaged boundaries while maintaining safety and usability. This document explains how errors propagate through the three-layer architecture and the patterns used at each layer. + +## Core Challenge: Managed/Unmanaged Boundary + +The fundamental challenge in SkiaSharp error handling is preventing invalid operations from reaching native code, where they would cause crashes. + +### Safety Strategy: Validate in C# + +**SkiaSharp's approach:** +- **C# layer performs the critical validation** where the native API would crash or misbehave, but many high-volume helpers skip extra checks and simply propagate native return values. +- **C API is a minimal wrapper** - no exception handling, no broad input validation. +- **Performance optimization** - most validation happens once (in managed code) when it is needed. + +```csharp +// Representative guard: managed code blocks obvious misuse, but not every failure path throws. +public void DrawRect(SKRect rect, SKPaint paint) +{ + // Validation happens here + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); +} +``` + +```cpp +// C API trusts C# - no validation needed +SK_C_API 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)); +} +``` + +**Why this works:** +1. C# wrappers are the only supported entry point into the C API. +2. The code paths that *must* guard inputs (for example null pointers) do so before invoking native code. +3. Remaining failures are surfaced through native return values (`false`, `nullptr`, default structs) which managed callers can observe. +4. Keeping the C API thin avoids redundant checks and simplifies maintenance. + +## Error Handling Strategy by Layer + +```mermaid +graph TB + subgraph CSharp["C# Layer - Safety Boundary"] + CS1[Validate critical inputs] + CS2[Call P/Invoke] + CS3[Process native result] + CS4{Failure?} + CS5[Throw or propagate] + CS6[Return success value] + + CS1 --> CS2 + CS2 --> CS3 + CS3 --> CS4 + CS4 -->|Yes| CS5 + CS4 -->|No| CS6 + end + + subgraph CAPI["C API Layer - Minimal Wrapper"] + C1[Convert types] + C2[Call C++ method] + C3[Return result] + + C1 --> C2 + C2 --> C3 + end + + subgraph CPP["C++ Skia Layer"] + CPP1[Execute operation] + CPP2{Error?} + CPP3[Throw exception] + CPP4[Return result] + + CPP1 --> CPP2 + CPP2 -->|Yes| CPP3 + CPP2 -->|No| CPP4 + end + + CS3 -.->|P/Invoke| C1 + C2 -.->|Direct call| CPP1 + CPP3 -.->|Would propagate!| CS3 + C3 -.->|Result| CS4 + + style CSharp fill:#e1f5e1 + style CAPI fill:#fff4e1 + style CPP fill:#e1e8f5 + style CS1 fill:#90ee90 + style CS2 fill:#90ee90 + style CS6 fill:#ffe1e1 +``` + +**Layer characteristics:** + +``` +┌─────────────────────────────────────────────────┐ +│ C# Layer - SAFETY BOUNDARY │ +│ ✓ Guards inputs that would crash native code │ +│ ✓ Interprets native return values │ +│ ✓ Throws when APIs guarantee exceptions │ +│ → Defines managed-facing error semantics │ +└─────────────────┬───────────────────────────────┘ + │ +┌─────────────────▼───────────────────────────────┐ +│ C API Layer - MINIMAL WRAPPER │ +│ ✓ Converts opaque pointers to C++ types │ +│ ✓ Calls C++ methods directly │ +│ ✓ Returns results to C# │ +│ ✗ Does NOT validate parameters │ +│ ✗ Does NOT catch exceptions │ +│ → Trusts C# has validated everything │ +└─────────────────┬───────────────────────────────┘ + │ +┌─────────────────▼───────────────────────────────┐ +│ C++ Skia Layer │ +│ ✓ May throw C++ exceptions │ +│ ✓ Uses assertions for invalid states │ +│ ✓ Relies on RAII for cleanup │ +│ → Only receives valid inputs from C# via C API │ +└─────────────────────────────────────────────────┘ +``` + +## Layer 1: C# Error Handling + +The C# layer is responsible for: +1. **Proactive validation** before calling native code +2. **Interpreting error signals** from C API +3. **Throwing appropriate C# exceptions** + +### Pattern 1: Parameter Validation + +Validate parameters **before** P/Invoking to avoid undefined behavior in native code. + +```csharp +public class SKCanvas : SKObject +{ + public void DrawRect(SKRect rect, SKPaint paint) + { + // Validate parameters before calling native code + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + + // Check object state + if (Handle == IntPtr.Zero) + throw new ObjectDisposedException("SKCanvas"); + + // Call native - at this point parameters are valid + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); + } +} +``` + +**Common validations:** +- Null checks for reference parameters (most common) +- Handle != IntPtr.Zero checks in constructors after native creation +- Range checks for numeric values (less common) +- Array bounds checks (where applicable) + +### Pattern 2: Factory Method Null Returns + +**Important:** Static factory methods return `null` on failure, they do NOT throw exceptions. + +```csharp +public class SKImage : SKObject, ISKReferenceCounted +{ + // Factory method returns null on failure + 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 + } + + // ✅ CORRECT usage - always check for null + public static void Example() + { + var image = SKImage.FromEncodedData(data); + if (image == null) + throw new InvalidOperationException("Failed to decode image"); + + // Safe to use image + canvas.DrawImage(image, 0, 0); + } + + // Boolean return methods let caller decide + public bool ReadPixels(SKImageInfo dstInfo, IntPtr dstPixels, int dstRowBytes, int srcX, int srcY) + { + // Boolean return indicates success/failure + var success = SkiaApi.sk_image_read_pixels( + Handle, &dstInfo, dstPixels, dstRowBytes, srcX, srcY, + SKImageCachingHint.Allow); + + return success; // Caller can check and decide what to do + } +} +``` + +**Affected Methods:** All static factory methods follow this pattern: +- `SKImage.FromEncodedData()` - Returns null on decode failure +- `SKImage.FromBitmap()` - Returns null on failure +- `SKSurface.Create()` - Returns null on allocation failure +- `SKShader.CreateLinearGradient()` - Returns null on failure +- And many more... + +### Pattern 3: Constructor Failures + +Constructors must ensure valid object creation or throw. + +```csharp +public class SKBitmap : SKObject +{ + public SKBitmap(SKImageInfo info) + : base(IntPtr.Zero, true) + { + var nInfo = SKImageInfoNative.FromManaged(ref info); + Handle = SkiaApi.sk_bitmap_new(); + + if (Handle == IntPtr.Zero) + throw new InvalidOperationException("Failed to create bitmap"); + + // Try to allocate pixels + if (!SkiaApi.sk_bitmap_try_alloc_pixels(Handle, &nInfo)) + { + // Clean up partial object + SkiaApi.sk_bitmap_destructor(Handle); + Handle = IntPtr.Zero; + throw new InvalidOperationException("Failed to allocate bitmap pixels"); + } + } +} +``` + +### Pattern 4: Disposal Safety + +Ensure disposal methods never throw. + +```csharp +protected override void DisposeNative() +{ + try + { + if (this is ISKReferenceCounted refcnt) + refcnt.SafeUnRef(); + // Never throw from dispose + } + catch + { + // Swallow exceptions in dispose + // Logging could happen here if available + } +} +``` + +### Common C# Exception Types + +| Exception | When to Use | +|-----------|-------------| +| `ArgumentNullException` | Null parameter passed | +| `ArgumentOutOfRangeException` | Numeric value out of valid range | +| `ArgumentException` | Invalid argument value | +| `ObjectDisposedException` | Operation on disposed object | +| `InvalidOperationException` | Object in wrong state or operation failed | +| `NotSupportedException` | Operation not supported on this platform | + +## Layer 2: C API Implementation (Actual) + +The C API layer is a **minimal wrapper** that: +1. **Converts types** - Opaque pointers to C++ types +2. **Calls C++ methods** - Direct pass-through +3. **Returns results** - Back to C# + +**It does NOT:** +- ❌ Validate parameters (C# does this) +- ❌ Catch exceptions (Skia rarely throws; C# prevents invalid inputs) +- ❌ Check for null pointers (C# ensures valid pointers) + +### Actual Pattern: Direct Pass-Through + +Most C API functions are simple wrappers with no error handling: + +```cpp +// Void methods - direct call +SK_C_API 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)); +} + +SK_C_API void sk_canvas_clear(sk_canvas_t* canvas, sk_color_t color) { + AsCanvas(canvas)->clear(color); +} + +SK_C_API void sk_paint_set_color(sk_paint_t* paint, sk_color_t color) { + AsPaint(paint)->setColor(color); +} +``` + +### Pattern: Boolean Return (Native Result) + +Some C++ methods naturally return bool - C API passes it through: + +```cpp +// C++ method returns bool, C API passes it through +SK_C_API bool sk_bitmap_try_alloc_pixels(sk_bitmap_t* bitmap, const sk_imageinfo_t* info) { + return AsBitmap(bitmap)->tryAllocPixels(AsImageInfo(info)); +} + +SK_C_API bool sk_image_read_pixels(const sk_image_t* image, const sk_imageinfo_t* dstInfo, + void* dstPixels, size_t dstRowBytes, int srcX, int srcY) { + return AsImage(image)->readPixels(AsImageInfo(dstInfo), dstPixels, dstRowBytes, srcX, srcY); +} +``` + +**Note:** C# checks the returned `bool` and throws exceptions if needed. + +### Pattern: Null Return (Factory Methods) + +Factory methods return `nullptr` naturally if creation fails: + +```cpp +// Returns nullptr if Skia factory fails +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()); +} + +SK_C_API sk_surface_t* sk_surface_new_raster(const sk_imageinfo_t* info) { + return ToSurface(SkSurfaces::Raster(AsImageInfo(info)).release()); +} + +SK_C_API sk_shader_t* sk_shader_new_linear_gradient(/*...*/) { + return ToShader(SkGradientShader::MakeLinear(/*...*/).release()); +} +``` + +**Note:** C# checks for `IntPtr.Zero` and throws `InvalidOperationException` if null. + +### Why No Exception Handling? + +**Design decision reasons:** +1. **Performance** - No overhead from try-catch blocks +2. **Simplicity** - Minimal code in C API layer +3. **Single responsibility** - C# owns all validation +4. **Skia rarely throws** - Most Skia functions don't throw exceptions +5. **Trust boundary** - C API trusts its only caller (C# wrapper) + +## Layer 3: C++ Skia Error Handling + +The C++ layer can use normal C++ error handling: +- Exceptions for exceptional cases +- Return values for expected failures +- Assertions for programming errors + +**Skia's approach:** +- Minimal exception usage (mostly for allocation failures) +- Return nullptr or false for failures +- Assertions (SK_ASSERT) for debug builds +- Graceful degradation when possible + +```cpp +// Skia C++ patterns +sk_sp SkImages::DeferredFromEncodedData(sk_sp data) { + if (!data) { + return nullptr; // Return null, don't throw + } + // ... create image or return nullptr on failure +} + +bool SkBitmap::tryAllocPixels(const SkImageInfo& info) { + // Returns false if allocation fails + return this->tryAllocPixelsInfo(info); +} +``` + +## Complete Error Flow Examples + +### Example 1: Drawing with Invalid Paint (Null Check) + +```csharp +// C# Layer - Validation +public void DrawRect(SKRect rect, SKPaint paint) +{ + if (paint == null) + throw new ArgumentNullException(nameof(paint)); // ✓ Caught here + + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); +} + +// If validation was missing: +// P/Invoke would pass IntPtr.Zero +// ↓ +// C API Layer - Defensive Check +SK_C_API void sk_canvas_draw_rect(sk_canvas_t* canvas, const sk_rect_t* rect, const sk_paint_t* paint) { + if (!canvas || !rect || !paint) + return; // ✓ Silently ignore - prevent crash + + AsCanvas(canvas)->drawRect(*AsRect(rect), *AsPaint(paint)); +} +``` + +### Example 2: Image Creation Failure + +```csharp +// C# Layer +public static SKImage FromEncodedData(SKData data) +{ + if (data == null) + throw new ArgumentNullException(nameof(data)); // ✓ Validate input + + var handle = SkiaApi.sk_image_new_from_encoded(data.Handle); + + if (handle == IntPtr.Zero) + throw new InvalidOperationException("Failed to decode image"); // ✓ Check result + + return GetObject(handle); +} + +// C API Layer +SK_C_API sk_image_t* sk_image_new_from_encoded(const sk_data_t* data) { + try { + auto image = SkImages::DeferredFromEncodedData(sk_ref_sp(AsData(data))); + return ToImage(image.release()); // Returns nullptr if failed + } catch (...) { + return nullptr; // ✓ Catch exceptions, return null + } +} + +// C++ Layer +sk_sp SkImages::DeferredFromEncodedData(sk_sp data) { + if (!data) { + return nullptr; // ✓ Return null on invalid input + } + + auto codec = SkCodec::MakeFromData(data); + if (!codec) { + return nullptr; // ✓ Decoding failed, return null + } + + return SkImages::DeferredFromCodec(std::move(codec)); +} +``` + +### Example 3: Parameter Validation Pattern + +```csharp +// C# Layer - Typical pattern (most methods) +public void DrawRect(SKRect rect, SKPaint paint) +{ + // Validates ONLY null reference parameters + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + + // Note: Does NOT check if Handle is disposed (IntPtr.Zero) + // Assumes object is valid if wrapper exists + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); +} + +// Rare pattern - only critical methods check disposal +public int Save() +{ + if (Handle == IntPtr.Zero) + throw new ObjectDisposedException("SKCanvas"); + return SkiaApi.sk_canvas_save(Handle); +} +``` + +### Example 4: Pixel Allocation Failure + +```csharp +// C# Layer +public class SKBitmap : SKObject +{ + public SKBitmap(SKImageInfo info) + : base(IntPtr.Zero, true) + { + var nInfo = SKImageInfoNative.FromManaged(ref info); + Handle = SkiaApi.sk_bitmap_new(); + + if (Handle == IntPtr.Zero) + throw new InvalidOperationException("Failed to create bitmap"); // ✓ Check creation + + if (!SkiaApi.sk_bitmap_try_alloc_pixels(Handle, &nInfo)) + { + // ✓ Allocation failed - clean up and throw + SkiaApi.sk_bitmap_destructor(Handle); + Handle = IntPtr.Zero; + throw new InvalidOperationException( + $"Failed to allocate pixels for {info.Width}x{info.Height} bitmap"); + } + } +} + +// C API Layer - Pass through the bool from C++ +SK_C_API bool sk_bitmap_try_alloc_pixels(sk_bitmap_t* bitmap, const sk_imageinfo_t* info) { + return AsBitmap(bitmap)->tryAllocPixels(AsImageInfo(info)); +} + +// C++ Layer +bool SkBitmap::tryAllocPixels(const SkImageInfo& info) { + // Returns false if allocation fails (out of memory, invalid size, etc.) + if (!this->setInfo(info)) { + return false; + } + + auto allocator = SkBitmapAllocator::Make(info); + if (!allocator) { + return false; // ✓ Allocation failed + } + + fPixelRef = std::move(allocator); + return true; +} +``` + +**Note:** C++ method returns bool naturally, C API passes it through, C# checks it. + +## Error Handling Best Practices + +### For C# Layer + +✅ **DO:** +- Validate reference parameters (null checks) before P/Invoke +- Check Handle != IntPtr.Zero in **constructors** after native creation +- Inspect native return values and choose whether to propagate them or throw, matching existing patterns +- Throw appropriate exception types matching existing API patterns +- Use meaningful error messages when throwing +- Provide context in exception messages + +❌ **DON'T:** +- Skip null checks for reference parameters (C API won't check) +- Ignore return values from factory/try methods +- Throw from Dispose/finalizer +- Use generic exceptions without context +- Assume C API will validate anything + +**Actual code patterns:** + +```csharp +// Pattern 1: Validate reference parameters (most common) +public void DrawRect(SKRect rect, SKPaint paint) +{ + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); + // Note: Does NOT check if Handle is disposed - assumes valid object +} + +// Pattern 2: Check Handle in constructors +public SKPaint() + : this(SkiaApi.sk_compatpaint_new(), true) +{ + if (Handle == IntPtr.Zero) + throw new InvalidOperationException("Unable to create a new SKPaint instance."); +} + +// Pattern 3: Factory methods return null (don't 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 +} +``` + +**Note:** Most instance methods do NOT check if the object is disposed (Handle == IntPtr.Zero). They assume the object is valid if it exists. The primary validation is null-checking reference parameters. + +### For C API Layer + +✅ **DO:** +- Keep implementations simple and direct +- Pass through natural return values (bool, null) +- Trust that C# has validated everything +- Use `sk_ref_sp()` when passing ref-counted objects to C++ +- Call `.release()` on `sk_sp` when returning + +❌ **DON'T:** +- Add unnecessary validation (C# already did it) +- Add try-catch blocks unless truly needed +- Modify Skia return values +- Throw exceptions + +**Current implementation pattern:** +```cpp +// Simple, direct wrapper - trusts C# validation +SK_C_API 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)); +} + +// Pass through natural bool return +SK_C_API bool sk_bitmap_try_alloc_pixels(sk_bitmap_t* bitmap, const sk_imageinfo_t* info) { + return AsBitmap(bitmap)->tryAllocPixels(AsImageInfo(info)); +} + +// Factory returns nullptr naturally on failure +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()); +} +``` + +### For Both Layers + +✅ **DO:** +- Fail fast with clear errors +- Provide useful error messages +- Clean up resources on failure +- Document error conditions +- Test error paths + +❌ **DON'T:** +- Silently ignore errors (unless documented) +- Leave objects in invalid state +- Leak resources on error paths + +## Debugging Failed Operations + +### When a C# call fails: + +1. **Check C# validation** - Did parameter validation catch it? +2. **Check return value** - Is C API returning error? +3. **Check C API implementation** - Is it catching exceptions? +4. **Check C++ behavior** - What does Skia return? +5. **Check documentation** - Is the operation supported? + +### Common Failure Scenarios + +| Symptom | Likely Cause | Solution | +|---------|--------------|----------| +| `ArgumentNullException` | Null parameter | Check calling code | +| `ObjectDisposedException` | Using disposed object | Check lifecycle | +| `InvalidOperationException` | C API returned error | Check C API return value | +| Crash in native code | Invalid parameter from C# | Add/fix C# validation | +| Silent failure | Error not propagated | Add return value checks | + +**Note:** If crashes occur in native code, it usually means C# validation is missing or incomplete. + +## Platform-Specific Error Handling + +Some operations may fail on specific platforms: + +```csharp +public static GRContext CreateGl() +{ + var handle = SkiaApi.gr_direct_context_make_gl(IntPtr.Zero); + + if (handle == IntPtr.Zero) + { + #if __IOS__ || __TVOS__ + throw new PlatformNotSupportedException("OpenGL not supported on iOS/tvOS"); + #else + throw new InvalidOperationException("Failed to create OpenGL context"); + #endif + } + + return GetObject(handle); +} +``` + +## Summary + +Error handling in SkiaSharp still relies on the managed layer as the safety boundary, but each layer has clearly defined responsibilities: + +1. **C# Layer (Safety Boundary)**: + - Guards reference parameters with null checks before calling native code + - Validates Handle creation in constructors (throws if Handle == IntPtr.Zero after native creation) + - Inspects native return values and translates them into either propagated results (`null`/`false`) or managed exceptions, depending on the API contract + - Establishes the public behavior (throwing vs. returning status) + +2. **C API Layer (Minimal Wrapper)**: + - Calls directly into the C++ API with almost no additional logic + - Avoids catching exceptions or duplicating validation + - Performs only the type conversions needed for P/Invoke + +3. **C++ Skia Layer**: + - Executes the actual work, optionally returning `nullptr`/`false` on failure + - Relies on upstream layers to pass valid arguments; assertions fire if they do not + +Key principles: +- **C# defines the managed contract** – check similar APIs to see whether they throw or return status +- **C API stays thin** – no redundant validation or exception handling +- **Prefer fail-fast guards** for inputs we know will crash native code +- **Propagate native status codes** when the existing API surface expects nullable/bool results +- **Provide clear exception messages** when throwing +- **Do not throw from Dispose/finalizers** – follow current suppression pattern + +## Next Steps + +- See [Memory Management](memory-management.md) for cleanup on error paths +- See [Adding New APIs](adding-new-apis.md) for implementing error handling in new bindings diff --git a/design/layer-mapping.md b/design/layer-mapping.md new file mode 100644 index 0000000000..65ceb78b58 --- /dev/null +++ b/design/layer-mapping.md @@ -0,0 +1,604 @@ +# Layer Mapping Reference + +> **Quick Start:** For a practical tutorial, see [QUICKSTART.md](QUICKSTART.md) +> **Quick Reference:** For a 2-minute overview, see [AGENTS.md](../AGENTS.md) + +## TL;DR + +**Naming conventions across layers:** + +- **C++:** `SkCanvas::drawRect(const SkRect& rect, const SkPaint& paint)` +- **C API:** `sk_canvas_draw_rect(sk_canvas_t* canvas, const sk_rect_t* rect, const sk_paint_t* paint)` +- **C#:** `SKCanvas.DrawRect(SKRect rect, SKPaint paint)` + +**Type mapping patterns:** +- C++ class → C opaque pointer → C# wrapper class +- `SkType` → `sk_type_t*` → `SKType` +- Value types map directly (int, float, bool, enums) + +**Function naming:** +- C++: Method names: `drawRect()`, `clear()` +- C API: `sk__`: `sk_canvas_draw_rect()`, `sk_canvas_clear()` +- C#: PascalCase methods: `DrawRect()`, `Clear()` + +See tables below for complete mappings of types, functions, and enums. + +--- + +## Introduction + +This document provides detailed mappings between the three layers of SkiaSharp, serving as a quick reference for understanding how types, functions, and patterns translate across layer boundaries. + +## Type Naming Conventions + +### C++ to C API Mapping + +| C++ Type | C API Type | Notes | +|----------|------------|-------| +| `SkCanvas` | `sk_canvas_t*` | Opaque pointer | +| `SkPaint` | `sk_paint_t*` | Opaque pointer | +| `SkImage` | `sk_image_t*` | Opaque pointer | +| `SkBitmap` | `sk_bitmap_t*` | Opaque pointer | +| `SkPath` | `sk_path_t*` | Opaque pointer | +| `SkRect` | `sk_rect_t` | Value type struct | +| `SkPoint` | `sk_point_t` | Value type struct | +| `SkColor` | `sk_color_t` | `uint32_t` typedef | +| `SkScalar` | `float` | Float primitive | +| `bool` | `bool` | Boolean primitive | + +**Pattern:** `SkType` → `sk_type_t*` (for classes) or `sk_type_t` (for structs) + +### C API to C# Mapping + +| C API Type | C# Type Alias | Actual C# Type | Notes | +|------------|---------------|----------------|-------| +| `sk_canvas_t*` | `sk_canvas_t` | `IntPtr` | Handle to native object | +| `sk_paint_t*` | `sk_paint_t` | `IntPtr` | Handle to native object | +| `sk_image_t*` | `sk_image_t` | `IntPtr` | Handle to native object | +| `sk_rect_t` | `SKRect` | `struct SKRect` | Marshaled value type | +| `sk_point_t` | `SKPoint` | `struct SKPoint` | Marshaled value type | +| `sk_color_t` | `SKColor` | `uint` | Primitive | +| `float` | `float` | `float` | Primitive | +| `bool` | `bool` | `bool` | Marshaled as I1 | + +**Pattern:** `sk_type_t*` → `IntPtr` handle → `SKType` C# wrapper class + +### Complete Three-Layer Mapping + +| Concept | C++ Layer | C API Layer | C# P/Invoke Layer | C# Wrapper Layer | +|---------|-----------|-------------|-------------------|------------------| +| **Canvas** | `SkCanvas*` | `sk_canvas_t*` | `sk_canvas_t` (IntPtr) | `SKCanvas` | +| **Paint** | `SkPaint*` | `sk_paint_t*` | `sk_paint_t` (IntPtr) | `SKPaint` | +| **Image** | `sk_sp` | `sk_image_t*` | `sk_image_t` (IntPtr) | `SKImage` | +| **Rectangle** | `SkRect` | `sk_rect_t` | `SKRect` | `SKRect` | +| **Point** | `SkPoint` | `sk_point_t` | `SKPoint` | `SKPoint` | +| **Color** | `SkColor` | `sk_color_t` | `uint` | `SKColor` (uint) | + +## Function Naming Conventions + +### Pattern: `sk__[_
]` + +**Examples:** + +| C++ Method | C API Function | C# Method | +|------------|----------------|-----------| +| `SkCanvas::drawRect()` | `sk_canvas_draw_rect()` | `SKCanvas.DrawRect()` | +| `SkPaint::getColor()` | `sk_paint_get_color()` | `SKPaint.Color` (get) | +| `SkPaint::setColor()` | `sk_paint_set_color()` | `SKPaint.Color` (set) | +| `SkImage::width()` | `sk_image_get_width()` | `SKImage.Width` | +| `SkCanvas::save()` | `sk_canvas_save()` | `SKCanvas.Save()` | + +**C# Naming Conventions:** +- Methods: PascalCase (`DrawRect`, not `drawRect`) +- Properties instead of get/set methods +- Parameters: camelCase +- Events: PascalCase with `Event` suffix (if applicable) + +## File Organization Mapping + +### Canvas Example + +| Layer | File Path | Contents | +|-------|-----------|----------| +| **C++ API** | `externals/skia/include/core/SkCanvas.h` | `class SkCanvas` declaration | +| **C++ Impl** | `externals/skia/src/core/SkCanvas.cpp` | `SkCanvas` implementation | +| **C API Header** | `externals/skia/include/c/sk_canvas.h` | `sk_canvas_*` function declarations | +| **C API Impl** | `externals/skia/src/c/sk_canvas.cpp` | `sk_canvas_*` function implementations | +| **C# P/Invoke** | `binding/SkiaSharp/SkiaApi.cs` or `SkiaApi.generated.cs` | `sk_canvas_*` P/Invoke declarations | +| **C# Wrapper** | `binding/SkiaSharp/SKCanvas.cs` | `SKCanvas` class implementation | + +### Type Conversion Helpers + +| Layer | Location | Purpose | +|-------|----------|---------| +| **C API** | `externals/skia/src/c/sk_types_priv.h` | Type conversion macros: `AsCanvas()`, `ToCanvas()` | +| **C#** | `binding/SkiaSharp/SKObject.cs` | Base class with handle management | +| **C#** | `binding/SkiaSharp/SkiaApi.cs` | Type aliases: `using sk_canvas_t = IntPtr;` | + +## Type Conversion Macros (C API Layer) + +### Macro Definitions + +```cpp +// In sk_types_priv.h + +#define DEF_CLASS_MAP(SkType, sk_type, Name) + // Generates: + // static inline const SkType* As##Name(const sk_type* t) + // static inline SkType* As##Name(sk_type* t) + // static inline const sk_type* To##Name(const SkType* t) + // static inline sk_type* To##Name(SkType* t) + +// Example: +DEF_CLASS_MAP(SkCanvas, sk_canvas_t, Canvas) +// Generates: AsCanvas(), ToCanvas() +``` + +### Common Conversion Macros + +| Macro | Purpose | Example | +|-------|---------|---------| +| `AsCanvas(sk_canvas_t*)` | Convert C type to C++ | `AsCanvas(ccanvas)` → `SkCanvas*` | +| `ToCanvas(SkCanvas*)` | Convert C++ type to C | `ToCanvas(canvas)` → `sk_canvas_t*` | +| `AsPaint(sk_paint_t*)` | Convert C type to C++ | `AsPaint(cpaint)` → `SkPaint*` | +| `ToPaint(SkPaint*)` | Convert C++ type to C | `ToPaint(paint)` → `sk_paint_t*` | +| `AsImage(sk_image_t*)` | Convert C type to C++ | `AsImage(cimage)` → `SkImage*` | +| `ToImage(SkImage*)` | Convert C++ type to C | `ToImage(image)` → `sk_image_t*` | +| `AsRect(sk_rect_t*)` | Convert C type to C++ | `AsRect(crect)` → `SkRect*` | +| `ToRect(SkRect*)` | Convert C++ type to C | `ToRect(rect)` → `sk_rect_t*` | + +**Full list of generated macros:** + +```cpp +// Pointer types +DEF_CLASS_MAP(SkCanvas, sk_canvas_t, Canvas) +DEF_CLASS_MAP(SkPaint, sk_paint_t, Paint) +DEF_CLASS_MAP(SkImage, sk_image_t, Image) +DEF_CLASS_MAP(SkBitmap, sk_bitmap_t, Bitmap) +DEF_CLASS_MAP(SkPath, sk_path_t, Path) +DEF_CLASS_MAP(SkShader, sk_shader_t, Shader) +DEF_CLASS_MAP(SkData, sk_data_t, Data) +DEF_CLASS_MAP(SkSurface, sk_surface_t, Surface) +// ... and many more + +// Value types +DEF_MAP(SkRect, sk_rect_t, Rect) +DEF_MAP(SkIRect, sk_irect_t, IRect) +DEF_MAP(SkPoint, sk_point_t, Point) +DEF_MAP(SkIPoint, sk_ipoint_t, IPoint) +DEF_MAP(SkColor4f, sk_color4f_t, Color4f) +// ... and many more +``` + +## Parameter Passing Patterns + +### By Value vs By Pointer/Reference + +| C++ Parameter | C API Parameter | C# Parameter | Notes | +|---------------|-----------------|--------------|-------| +| `int x` | `int x` | `int x` | Simple value | +| `bool flag` | `bool flag` | `[MarshalAs(UnmanagedType.I1)] bool flag` | Bool needs marshaling | +| `const SkRect& rect` | `const sk_rect_t* rect` | `sk_rect_t* rect` or `ref SKRect rect` | Struct by pointer | +| `const SkPaint& paint` | `const sk_paint_t* paint` | `sk_paint_t paint` (IntPtr) | Object handle | +| `SkRect* outRect` | `sk_rect_t* outRect` | `sk_rect_t* outRect` or `out SKRect outRect` | Output parameter | + +### Ownership Transfer Patterns + +| C++ Pattern | C API Pattern | C# Pattern | Ownership | +|-------------|---------------|------------|-----------| +| `const SkPaint&` | `const sk_paint_t*` | `SKPaint paint` | Borrowed (no transfer) | +| `new SkCanvas()` | `sk_canvas_t* sk_canvas_new()` | `new SKCanvas()` | Caller owns | +| `sk_sp` returns | `sk_image_t* sk_image_new()` | `SKImage.FromX()` | Caller owns (ref count = 1) | +| Takes `sk_sp` | `sk_data_t*` with `sk_ref_sp()` | `SKData data` | Shared (ref count++) | + +## Memory Management Patterns + +### Owned Objects (Delete on Dispose) + +| Layer | Pattern | Example | +|-------|---------|---------| +| **C++** | `new`/`delete` | `auto canvas = new SkCanvas(bitmap); delete canvas;` | +| **C API** | `_new()`/`_delete()` | `sk_canvas_t* c = sk_canvas_new(); sk_canvas_delete(c);` | +| **C#** | Constructor/`Dispose()` | `var c = new SKCanvas(bitmap); c.Dispose();` | + +**C# Implementation:** +```csharp +public class SKCanvas : SKObject +{ + public SKCanvas(SKBitmap bitmap) : base(IntPtr.Zero, true) + { + Handle = SkiaApi.sk_canvas_new_from_bitmap(bitmap.Handle); + } + + protected override void DisposeNative() + { + SkiaApi.sk_canvas_destroy(Handle); + } +} +``` + +### Reference-Counted Objects (Unref on Dispose) + +| Layer | Pattern | Example | +|-------|---------|---------| +| **C++** | `sk_sp` or `ref()`/`unref()` | `sk_sp img = ...; // auto unref` | +| **C API** | `_ref()`/`_unref()` | `sk_image_ref(img); sk_image_unref(img);` | +| **C#** | `ISKReferenceCounted` | `var img = SKImage.FromX(); img.Dispose();` | + +**C# Implementation:** +```csharp +public class SKImage : SKObject, ISKReferenceCounted +{ + public static SKImage FromBitmap(SKBitmap bitmap) + { + var handle = SkiaApi.sk_image_new_from_bitmap(bitmap.Handle); + return GetObject(handle); // Ref count = 1 + } + + // DisposeNative inherited from SKObject calls SafeUnRef() for ISKReferenceCounted +} +``` + +### Non-Owning References + +| Layer | Pattern | Example | +|-------|---------|---------| +| **C++** | Raw pointer from getter | `SkSurface* s = canvas->getSurface();` | +| **C API** | Pointer without ownership | `sk_surface_t* s = sk_canvas_get_surface(c);` | +| **C#** | `OwnsHandle = false` | `var s = canvas.Surface; // non-owning wrapper` | + +**C# Implementation:** +```csharp +public SKSurface Surface +{ + get { + var handle = SkiaApi.sk_canvas_get_surface(Handle); + return GetOrAddObject(handle, owns: false, (h, o) => new SKSurface(h, o)); + } +} +``` + +## Common API Patterns + +### Pattern 1: Simple Method Call + +```cpp +// C++ +void SkCanvas::clear(SkColor color); +``` + +```cpp +// C API +SK_C_API void sk_canvas_clear(sk_canvas_t* canvas, sk_color_t color); + +void sk_canvas_clear(sk_canvas_t* canvas, sk_color_t color) { + AsCanvas(canvas)->clear(color); +} +``` + +```csharp +// C# P/Invoke +[DllImport("libSkiaSharp")] +public static extern void sk_canvas_clear(sk_canvas_t canvas, uint color); + +// C# Wrapper +public void Clear(SKColor color) +{ + SkiaApi.sk_canvas_clear(Handle, (uint)color); +} +``` + +### Pattern 2: Property Get + +```cpp +// C++ +int SkImage::width() const; +``` + +```cpp +// C API +SK_C_API int sk_image_get_width(const sk_image_t* image); + +int sk_image_get_width(const sk_image_t* image) { + return AsImage(image)->width(); +} +``` + +```csharp +// C# P/Invoke +[DllImport("libSkiaSharp")] +public static extern int sk_image_get_width(sk_image_t image); + +// C# Wrapper +public int Width => SkiaApi.sk_image_get_width(Handle); +``` + +### Pattern 3: Property Set + +```cpp +// C++ +void SkPaint::setColor(SkColor color); +``` + +```cpp +// C API +SK_C_API void sk_paint_set_color(sk_paint_t* paint, sk_color_t color); + +void sk_paint_set_color(sk_paint_t* paint, sk_color_t color) { + AsPaint(paint)->setColor(color); +} +``` + +```csharp +// C# P/Invoke +[DllImport("libSkiaSharp")] +public static extern void sk_paint_set_color(sk_paint_t paint, uint color); + +// C# Wrapper +public SKColor Color +{ + get => (SKColor)SkiaApi.sk_paint_get_color(Handle); + set => SkiaApi.sk_paint_set_color(Handle, (uint)value); +} +``` + +### Pattern 4: Factory Method (Owned) + +```cpp +// C++ +SkCanvas* SkCanvas::MakeRasterDirect(const SkImageInfo& info, void* pixels, size_t rowBytes); +``` + +```cpp +// C API +SK_C_API sk_canvas_t* sk_canvas_new_from_raster( + const sk_imageinfo_t* info, void* pixels, size_t rowBytes); + +sk_canvas_t* sk_canvas_new_from_raster( + const sk_imageinfo_t* info, void* pixels, size_t rowBytes) +{ + return ToCanvas(SkCanvas::MakeRasterDirect(AsImageInfo(info), pixels, rowBytes).release()); +} +``` + +```csharp +// C# P/Invoke +[DllImport("libSkiaSharp")] +public static extern sk_canvas_t sk_canvas_new_from_raster( + sk_imageinfo_t* info, IntPtr pixels, IntPtr rowBytes); + +// C# Wrapper +public static SKCanvas Create(SKImageInfo info, IntPtr pixels, int rowBytes) +{ + var nInfo = SKImageInfoNative.FromManaged(ref info); + var handle = SkiaApi.sk_canvas_new_from_raster(&nInfo, pixels, (IntPtr)rowBytes); + if (handle == IntPtr.Zero) + throw new InvalidOperationException("Failed to create canvas"); + return new SKCanvas(handle, true); +} +``` + +### Pattern 5: Factory Method (Reference-Counted) + +```cpp +// C++ +sk_sp SkImages::RasterFromBitmap(const SkBitmap& bitmap); +``` + +```cpp +// C API +SK_C_API sk_image_t* sk_image_new_from_bitmap(const sk_bitmap_t* bitmap); + +sk_image_t* sk_image_new_from_bitmap(const sk_bitmap_t* bitmap) { + return ToImage(SkImages::RasterFromBitmap(*AsBitmap(bitmap)).release()); +} +``` + +```csharp +// C# P/Invoke +[DllImport("libSkiaSharp")] +public static extern sk_image_t sk_image_new_from_bitmap(sk_bitmap_t bitmap); + +// C# Wrapper +public static SKImage FromBitmap(SKBitmap bitmap) +{ + if (bitmap == null) + throw new ArgumentNullException(nameof(bitmap)); + + var handle = SkiaApi.sk_image_new_from_bitmap(bitmap.Handle); + if (handle == IntPtr.Zero) + throw new InvalidOperationException("Failed to create image"); + + return GetObject(handle); // ISKReferenceCounted, ref count = 1 +} +``` + +### Pattern 6: Method with Struct Parameter + +```cpp +// C++ +void SkCanvas::drawRect(const SkRect& rect, const SkPaint& paint); +``` + +```cpp +// C API +SK_C_API void sk_canvas_draw_rect( + sk_canvas_t* canvas, const sk_rect_t* rect, const sk_paint_t* paint); + +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)); +} +``` + +```csharp +// C# P/Invoke +[DllImport("libSkiaSharp")] +public static extern void sk_canvas_draw_rect( + sk_canvas_t canvas, sk_rect_t* rect, sk_paint_t paint); + +// C# Wrapper +public unsafe void DrawRect(SKRect rect, SKPaint paint) +{ + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + + SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle); +} +``` + +### Pattern 7: Output Parameter + +```cpp +// C++ +bool SkCanvas::getLocalClipBounds(SkRect* bounds) const; +``` + +```cpp +// C API +SK_C_API bool sk_canvas_get_local_clip_bounds(sk_canvas_t* canvas, sk_rect_t* bounds); + +bool sk_canvas_get_local_clip_bounds(sk_canvas_t* canvas, sk_rect_t* bounds) { + return AsCanvas(canvas)->getLocalClipBounds(AsRect(bounds)); +} +``` + +```csharp +// C# P/Invoke +[DllImport("libSkiaSharp")] +[return: MarshalAs(UnmanagedType.I1)] +public static extern bool sk_canvas_get_local_clip_bounds( + sk_canvas_t canvas, sk_rect_t* bounds); + +// C# Wrapper +public unsafe bool TryGetLocalClipBounds(out SKRect bounds) +{ + fixed (SKRect* b = &bounds) + { + return SkiaApi.sk_canvas_get_local_clip_bounds(Handle, b); + } +} + +public SKRect LocalClipBounds +{ + get { + TryGetLocalClipBounds(out var bounds); + return bounds; + } +} +``` + +## Enum Mapping + +Enums typically map 1:1 across all layers: + +| C++ Enum | C API Enum | C# Enum | +|----------|------------|---------| +| `SkCanvas::PointMode` | `sk_point_mode_t` | `SKPointMode` | +| `SkBlendMode` | `sk_blendmode_t` | `SKBlendMode` | +| `SkColorType` | `sk_colortype_t` | `SKColorType` | + +```cpp +// C++ +enum class SkBlendMode { + kClear, + kSrc, + kDst, + // ... +}; +``` + +```cpp +// C API +typedef enum { + SK_BLENDMODE_CLEAR = 0, + SK_BLENDMODE_SRC = 1, + SK_BLENDMODE_DST = 2, + // ... +} sk_blendmode_t; +``` + +```csharp +// C# +public enum SKBlendMode +{ + Clear = 0, + Src = 1, + Dst = 2, + // ... +} +``` + +**Cast pattern in C API:** +```cpp +void sk_canvas_draw_color(sk_canvas_t* canvas, sk_color_t color, sk_blendmode_t mode) { + AsCanvas(canvas)->drawColor(color, (SkBlendMode)mode); +} +``` + +## Struct Mapping + +Value structs also map across layers: + +```cpp +// C++ +struct SkRect { + float fLeft, fTop, fRight, fBottom; +}; +``` + +```cpp +// C API +typedef struct { + float left, top, right, bottom; +} sk_rect_t; +``` + +```csharp +// C# +[StructLayout(LayoutKind.Sequential)] +public struct SKRect +{ + public float Left; + public float Top; + public float Right; + public float Bottom; + + // Plus constructors, properties, methods +} +``` + +## Quick Reference: Type Categories + +### Pointer Types (Objects) + +| Category | C++ | C API | C# | Examples | +|----------|-----|-------|-----|----------| +| **Owned** | Class with new/delete | `_new()/_delete()` | `SKObject`, owns handle | SKCanvas, SKPaint, SKPath | +| **Ref-Counted** | `sk_sp`, `SkRefCnt` | `_ref()/_unref()` | `ISKReferenceCounted` | SKImage, SKShader, SKData | +| **Non-Owning** | Raw pointer | Pointer | `OwnsHandle=false` | Canvas.Surface getter | + +### Value Types + +| Category | C++ | C API | C# | Examples | +|----------|-----|-------|-----|----------| +| **Struct** | `struct` | `typedef struct` | `[StructLayout] struct` | SKRect, SKPoint, SKMatrix | +| **Primitive** | `int`, `float`, `bool` | `int`, `float`, `bool` | `int`, `float`, `bool` | Coordinates, sizes | +| **Enum** | `enum class` | `typedef enum` | `enum` | SKBlendMode, SKColorType | +| **Color** | `SkColor` (uint32_t) | `sk_color_t` (uint32_t) | `SKColor` (uint) | Color values | + +## Summary + +This layer mapping reference provides a quick lookup for: +- Type naming conventions across layers +- Function naming patterns +- File organization +- Type conversion macros +- Parameter passing patterns +- Memory management patterns +- Common API patterns + +For deeper understanding: +- [Architecture Overview](architecture-overview.md) - High-level architecture +- [Memory Management](memory-management.md) - Pointer types and ownership +- [Error Handling](error-handling.md) - Error propagation patterns +- [Adding New APIs](adding-new-apis.md) - Step-by-step guide diff --git a/design/memory-management.md b/design/memory-management.md new file mode 100644 index 0000000000..e0c44a38d5 --- /dev/null +++ b/design/memory-management.md @@ -0,0 +1,898 @@ +# Memory Management in SkiaSharp + +> **Quick Start:** For a practical tutorial, see [QUICKSTART.md](QUICKSTART.md) +> **Quick Reference:** For a 2-minute overview, see [AGENTS.md](../AGENTS.md) + +## TL;DR + +**Three pointer types determine memory management:** + +1. **Raw Pointers (Non-Owning)** - Borrowed references, no cleanup + - C++: `const SkType&` parameters, getter returns + - C#: `owns: false` in constructor + - Example: Paint parameter in `DrawRect(rect, paint)` + +2. **Owned Pointers (Unique)** - One owner, explicit delete + - C++: Mutable objects like Canvas, Paint, Bitmap + - C API: `sk_type_new()` / `sk_type_delete()` pairs + - C#: `DisposeNative()` calls delete/destroy + +3. **Reference-Counted Pointers (Shared)** - Two variants: + - **Virtual** (`SkRefCnt`): Image, Shader, Surface → 8-16 byte overhead + - **Non-Virtual** (`SkNVRefCnt`): Data, TextBlob → 4 byte overhead + - Both use `sk_sp` and ref/unref pattern + - C#: `ISKReferenceCounted` or `ISKNonVirtualReferenceCounted` + +**How to identify:** Check C++ class inheritance (`SkRefCnt`, `SkNVRefCnt`, or mutable type) + +**Critical:** Getting pointer type wrong → memory leaks or crashes + +--- + +## Introduction + +Understanding memory management is critical when working with SkiaSharp because it bridges managed C# code with unmanaged native code. This document explains the different pointer types used in Skia, how they map through the three layers, and how to properly manage object lifecycles. + +## Overview: Three Pointer Type Categories + +Skia uses three fundamental categories of pointer types for memory management: + +1. **Raw Pointers** - Non-owning references, caller manages lifetime +2. **Owned Pointers** - Unique ownership, owner responsible for deletion +3. **Reference-Counted Pointers** - Shared ownership via reference counting + +Understanding which category an API uses is essential for creating correct bindings. + +## Memory Lifecycle Visualizations + +### Lifecycle: Owned Pointer (Unique Ownership) + +```mermaid +sequenceDiagram + participant CS as C# Code + participant Wrapper as SKPaint Wrapper + participant PInvoke as P/Invoke + participant CAPI as C API + participant Native as Native SkPaint + + Note over CS,Native: Creation Phase + CS->>Wrapper: new SKPaint() + Wrapper->>PInvoke: sk_paint_new() + PInvoke->>CAPI: sk_paint_new() + CAPI->>Native: new SkPaint() + Native-->>CAPI: SkPaint* ptr + CAPI-->>PInvoke: sk_paint_t* handle + PInvoke-->>Wrapper: IntPtr handle + Wrapper-->>CS: SKPaint instance + Note over Wrapper: OwnsHandle = true + + Note over CS,Native: Usage Phase + CS->>Wrapper: SetColor(red) + Wrapper->>PInvoke: sk_paint_set_color(handle, red) + PInvoke->>CAPI: sk_paint_set_color(paint, red) + CAPI->>Native: paint->setColor(red) + + Note over CS,Native: Disposal Phase + CS->>Wrapper: Dispose() or GC + Wrapper->>PInvoke: sk_paint_delete(handle) + PInvoke->>CAPI: sk_paint_delete(paint) + CAPI->>Native: delete paint + Note over Native: Memory freed +``` + +**Key Points:** +- Single owner (C# wrapper) +- Explicit disposal required +- No reference counting +- Deterministic cleanup with `using` statement + +### Lifecycle: Reference-Counted Pointer (Shared Ownership) + +```mermaid +sequenceDiagram + participant CS1 as C# Object 1 + participant CS2 as C# Object 2 + participant Wrapper1 as SKImage Wrapper 1 + participant Wrapper2 as SKImage Wrapper 2 + participant PInvoke as P/Invoke + participant CAPI as C API + participant Native as Native SkImage + + Note over Native: RefCount = 1 + + Note over CS1,Native: First Reference + CS1->>Wrapper1: Create from native + Wrapper1->>PInvoke: sk_image_ref(handle) + PInvoke->>CAPI: sk_image_ref(image) + CAPI->>Native: image->ref() + Note over Native: RefCount = 2 + + Note over CS2,Native: Second Reference (Share) + CS1->>CS2: Pass image reference + CS2->>Wrapper2: Create from same handle + Wrapper2->>PInvoke: sk_image_ref(handle) + PInvoke->>CAPI: sk_image_ref(image) + CAPI->>Native: image->ref() + Note over Native: RefCount = 3 + + Note over CS1,Native: First Dispose + CS1->>Wrapper1: Dispose() + Wrapper1->>PInvoke: sk_image_unref(handle) + PInvoke->>CAPI: sk_image_unref(image) + CAPI->>Native: image->unref() + Note over Native: RefCount = 2
(Still alive) + + Note over CS2,Native: Second Dispose + CS2->>Wrapper2: Dispose() + Wrapper2->>PInvoke: sk_image_unref(handle) + PInvoke->>CAPI: sk_image_unref(image) + CAPI->>Native: image->unref() + Note over Native: RefCount = 1
(Original owner) + + Note over Native: Original unref()
RefCount = 0
Memory freed +``` + +**Key Points:** +- Multiple owners allowed +- Thread-safe reference counting +- Automatic cleanup when last reference dropped +- Each C# wrapper increments ref count + +### Lifecycle: Raw Pointer (Borrowed Reference) + +```mermaid +sequenceDiagram + participant CS as C# Code + participant Canvas as SKCanvas + participant Surface as SKSurface + participant PInvoke as P/Invoke + participant CAPI as C API + participant Native as Native Objects + + Note over CS,Native: Canvas owns Surface + CS->>Canvas: canvas.Surface + Canvas->>PInvoke: sk_canvas_get_surface(handle) + PInvoke->>CAPI: sk_canvas_get_surface(canvas) + CAPI->>Native: canvas->getSurface() + Native-->>CAPI: SkSurface* (non-owning) + CAPI-->>PInvoke: sk_surface_t* handle + PInvoke-->>Canvas: IntPtr handle + Canvas->>Surface: new SKSurface(handle, owns: false) + Note over Surface: OwnsHandle = false + Surface-->>CS: SKSurface instance + + Note over CS,Native: Use the borrowed reference + CS->>Surface: Use surface methods + + Note over CS,Native: Dispose wrapper (NOT native) + CS->>Surface: Dispose() + Note over Surface: Only wrapper disposed
Native object NOT freed
(Canvas still owns it) + + Note over CS,Native: Canvas disposal frees surface + CS->>Canvas: Dispose() + Canvas->>PInvoke: sk_canvas_destroy(handle) + Note over Native: Canvas AND Surface freed +``` + +**Key Points:** +- No ownership transfer +- Parent object owns the native resource +- C# wrapper is just a view +- Disposing wrapper doesn't free native memory + +## Pointer Type 1: Raw Pointers (Non-Owning) + +### Native C++ Layer + +**Identifier:** `SkType*` (raw pointer) + +**Characteristics:** +- Non-owning reference to an object +- Temporary access only +- Caller or another object owns the lifetime +- Never deleted by the receiver +- Common for parameters and temporary references + +**Example C++ APIs:** +```cpp +void SkCanvas::drawPaint(const SkPaint& paint); // Reference (equivalent to const SkPaint*) +SkSurface* SkCanvas::getSurface() const; // Raw pointer (canvas owns surface) +``` + +### C API Layer + +**C Type:** `sk_type_t*` (opaque pointer) + +**Characteristics:** +- Passed as-is without ownership transfer +- No ref/unref calls needed +- No destroy/delete function called on borrowed pointers + +**Example C API:** +```cpp +// Canvas doesn't own the paint, just uses it temporarily +SK_C_API void sk_canvas_draw_paint(sk_canvas_t* canvas, const sk_paint_t* paint); + +// Canvas owns the surface, returns non-owning pointer +SK_C_API sk_surface_t* sk_get_surface(sk_canvas_t* canvas); +``` + +### C# Wrapper Layer + +**C# Pattern:** `IntPtr` handle, `OwnsHandle = false` + +**Characteristics:** +- Wrapper created with `owns: false` parameter +- No disposal of native resource +- Wrapper lifecycle independent of native object +- Often registered in parent object's `OwnedObjects` collection + +**Example C# API:** +```csharp +public class SKCanvas : SKObject +{ + // Paint is borrowed, not owned by this method + public void DrawPaint(SKPaint paint) + { + if (paint == null) + throw new ArgumentNullException(nameof(paint)); + SkiaApi.sk_canvas_draw_paint(Handle, paint.Handle); + // Note: paint is NOT disposed here + } + + // Surface is owned by canvas, return non-owning wrapper + public SKSurface Surface + { + get { + var handle = SkiaApi.sk_get_surface(Handle); + // Create wrapper that doesn't own the handle + return SKObject.GetOrAddObject(handle, owns: false, + (h, o) => new SKSurface(h, o)); + } + } +} +``` + +**When to use:** +- Parameters that are only used during the function call +- Return values where the caller doesn't gain ownership +- Child objects owned by parent objects + +## Pointer Type 2: Owned Pointers (Unique Ownership) + +### Native C++ Layer + +**Identifiers:** +- `new SkType()` - Raw allocation, caller must delete +- `std::unique_ptr` - Unique ownership (rare in Skia) +- Most mutable Skia objects (SkCanvas, SkPaint, SkPath) + +**Characteristics:** +- One owner at a time +- Owner responsible for calling destructor +- RAII: destructor called automatically in C++ +- Ownership can transfer but not shared +- No reference counting overhead + +**Example C++ APIs:** +```cpp +SkCanvas* canvas = new SkCanvas(bitmap); // Caller owns, must delete +delete canvas; // Explicit cleanup + +SkPaint paint; // Stack allocation, auto-destroyed +``` + +### C API Layer + +**C Type:** `sk_type_t*` with `create`/`new` and `destroy`/`delete` functions + +**Characteristics:** +- Constructor functions: `sk_type_new_*` or `sk_type_create_*` +- Destructor functions: `sk_type_destroy` or `sk_type_delete` +- Caller must explicitly destroy what they create +- No ref/unref functions + +**Example C API:** +```cpp +// Owned pointer - create and destroy functions +SK_C_API sk_paint_t* sk_paint_new(void); +SK_C_API void sk_paint_delete(sk_paint_t* paint); + +SK_C_API sk_canvas_t* sk_canvas_new_from_bitmap(const sk_bitmap_t* bitmap); +SK_C_API void sk_canvas_destroy(sk_canvas_t* canvas); +``` + +**Implementation pattern:** +```cpp +// Creation allocates with new +sk_paint_t* sk_paint_new(void) { + return ToPaint(new SkPaint()); +} + +// Destruction uses delete +void sk_paint_delete(sk_paint_t* paint) { + delete AsPaint(paint); +} +``` + +### C# Wrapper Layer + +**C# Pattern:** `SKObject` with `OwnsHandle = true` and `DisposeNative()` override + +**Characteristics:** +- Created with `owns: true` parameter (default) +- Calls destroy/delete function in `DisposeNative()` +- NOT reference counted +- Implements `IDisposable` for deterministic cleanup + +**Example C# API:** +```csharp +public class SKPaint : SKObject, ISKSkipObjectRegistration +{ + public SKPaint() + : base(IntPtr.Zero, true) + { + Handle = SkiaApi.sk_paint_new(); + } + + protected override void DisposeNative() + { + SkiaApi.sk_paint_delete(Handle); + } +} + +public class SKCanvas : SKObject +{ + public SKCanvas(SKBitmap bitmap) + : base(IntPtr.Zero, true) + { + if (bitmap == null) + throw new ArgumentNullException(nameof(bitmap)); + Handle = SkiaApi.sk_canvas_new_from_bitmap(bitmap.Handle); + } + + protected override void DisposeNative() + { + SkiaApi.sk_canvas_destroy(Handle); + } +} +``` + +**Ownership transfer example:** +```csharp +// When canvas takes ownership of a drawable +public void DrawDrawable(SKDrawable drawable) +{ + // Canvas takes ownership, C# wrapper releases it + drawable.RevokeOwnership(this); + SkiaApi.sk_canvas_draw_drawable(Handle, drawable.Handle, ...); +} +``` + +**When to use:** +- Objects that are uniquely owned +- Mutable objects like SKCanvas, SKPaint, SKPath, SKBitmap +- Objects allocated by user with deterministic lifetime + +**Common types using owned pointers:** +- `SKCanvas` - Drawing surface +- `SKPaint` - Drawing attributes +- `SKPath` - Vector paths +- `SKBitmap` - Mutable bitmaps +- `SKRegion` - Clipping regions + +## Pointer Type 3: Reference-Counted Pointers (Shared Ownership) + +Reference-counted objects in Skia come in **two variants**: virtual (`SkRefCnt`) and non-virtual (`SkNVRefCnt`). Both use the same ref/unref pattern, but differ in size and virtual table overhead. + +### Variant A: Virtual Reference Counting (`SkRefCnt`) + +**Identifiers:** +- Inherits from `SkRefCnt` or `SkRefCntBase` +- Has virtual destructor (8-16 bytes overhead on most platforms) +- Used for polymorphic types that need virtual functions + +**Characteristics:** +- Shared ownership via reference counting +- Thread-safe reference counting (atomic operations) +- Object deleted when ref count reaches zero +- Used with `sk_sp` smart pointer +- Supports inheritance and virtual functions + +**Example C++ APIs:** +```cpp +// Virtual ref-counted base class +class SkImage : public SkRefCnt { + virtual ~SkImage() { } + // Virtual functions allowed +}; + +// Factory returns sk_sp (smart pointer) +sk_sp SkImages::DeferredFromEncodedData(sk_sp data); + +// Manual reference counting (rare, use sk_sp instead) +void ref() const; // Increment reference count +void unref() const; // Decrement, delete if zero +``` + +**Common types using SkRefCnt:** +- `SKImage` - Immutable images +- `SKShader` - Shader effects +- `SKColorFilter` - Color transformations +- `SKImageFilter` - Image effects +- `SKTypeface` - Font faces +- `SKSurface` - Drawing surfaces +- `SKPicture` - Recorded drawing commands + +### Variant B: Non-Virtual Reference Counting (`SkNVRefCnt`) + +**Identifiers:** +- Inherits from `SkNVRefCnt` (template) +- No virtual destructor (4 bytes overhead instead of 8-16) +- Used for final types that don't need virtual functions + +**Characteristics:** +- Same ref/unref semantics as `SkRefCnt` +- Thread-safe atomic reference counting +- Lighter weight (no vtable) +- Cannot be inherited from +- Used with `sk_sp` smart pointer (same as SkRefCnt) + +**Example C++ APIs:** +```cpp +// Non-virtual ref-counted (lighter weight) +class SK_API SkData final : public SkNVRefCnt { + // No virtual destructor needed + // Cannot be inherited from (final) +}; + +class SK_API SkTextBlob final : public SkNVRefCnt { ... }; +class SK_API SkVertices : public SkNVRefCnt { ... }; +class SkColorSpace : public SkNVRefCnt { ... }; +``` + +**Common types using SkNVRefCnt:** +- `SKData` - Immutable byte arrays +- `SKTextBlob` - Positioned text +- `SKVertices` - Vertex data for meshes +- `SKColorSpace` - Color space definitions + +**Why two variants exist:** +- `SkRefCnt`: Use when inheritance or virtual functions needed (most types) +- `SkNVRefCnt`: Use when performance matters and no virtuals needed (saves 4-12 bytes per object) + +### Smart Pointer Behavior (Both Variants) + +Both `SkRefCnt` and `SkNVRefCnt` work identically with `sk_sp`: + +```cpp +sk_sp image1 = SkImages::RasterFromBitmap(bitmap); // ref count = 1 +sk_sp image2 = image1; // ref() called, ref count = 2 +image1.reset(); // unref() called, ref count = 1 +image2.reset(); // unref() called, ref count = 0, object deleted +``` + +### C API Layer + +**C Type:** `sk_type_t*` with `ref` and `unref` functions + +**Characteristics:** +- Explicit ref/unref functions exposed +- Factory functions return objects with ref count = 1 +- Caller responsible for calling unref when done +- `sk_ref_sp()` helper increments ref count when passing to C++ + +**Example C API:** +```cpp +// Reference counting functions +SK_C_API void sk_image_ref(const sk_image_t* image); +SK_C_API void sk_image_unref(const sk_image_t* image); + +// Factory returns ref count = 1 (caller owns reference) +SK_C_API sk_image_t* sk_image_new_raster_copy( + const sk_imageinfo_t* info, + const void* pixels, + size_t rowBytes); +``` + +**Implementation pattern:** +```cpp +void sk_image_ref(const sk_image_t* cimage) { + AsImage(cimage)->ref(); +} + +void sk_image_unref(const sk_image_t* cimage) { + SkSafeUnref(AsImage(cimage)); // unref, handles null +} + +sk_image_t* sk_image_new_raster_copy(...) { + // SkImages::RasterFromPixmapCopy returns sk_sp + // .release() transfers ownership (doesn't unref) + return ToImage(SkImages::RasterFromPixmapCopy(...).release()); +} +``` + +**Important:** When passing ref-counted objects FROM C# TO C API: +```cpp +// If C++ expects sk_sp, must increment ref count +sk_image_t* sk_image_new_raster_data(..., sk_data_t* pixels, ...) { + // sk_ref_sp creates sk_sp and increments ref count + return ToImage(SkImages::RasterFromData(..., sk_ref_sp(AsData(pixels)), ...).release()); +} +``` + +### C# Wrapper Layer + +**C# Pattern:** Two interfaces for two ref-counting variants + +SkiaSharp distinguishes between the two C++ ref-counting variants: + +1. **`ISKReferenceCounted`** - For types inheriting from `SkRefCnt` (virtual) +2. **`ISKNonVirtualReferenceCounted`** - For types inheriting from `SkNVRefCnt` (non-virtual) + +**Characteristics:** +- Both interfaces trigger ref-counting disposal instead of delete +- `DisposeNative()` calls appropriate `unref` function +- Reference counting managed automatically +- Global handle dictionary ensures single wrapper per object +- Can be safely shared across multiple C# references + +**Virtual Ref-Counted Example (ISKReferenceCounted):** +```csharp +// For types inheriting from SkRefCnt (has vtable) +public class SKImage : SKObject, ISKReferenceCounted +{ + internal SKImage(IntPtr handle, bool owns) + : base(handle, owns) + { + } + + // Factory method returns owned image (ref count = 1) + public static SKImage FromPixelCopy(SKImageInfo info, IntPtr pixels, int rowBytes) + { + var nInfo = SKImageInfoNative.FromManaged(ref info); + // C API returns ref count = 1, we own it + return GetObject(SkiaApi.sk_image_new_raster_copy(&nInfo, (void*)pixels, rowBytes)); + } + + // No explicit DisposeNative override needed + // Base SKObject.DisposeNative calls SafeUnRef for ISKReferenceCounted +} +``` + +**Non-Virtual Ref-Counted Example (ISKNonVirtualReferenceCounted):** +```csharp +// For types inheriting from SkNVRefCnt (no vtable, lighter weight) +public class SKData : SKObject, ISKNonVirtualReferenceCounted +{ + internal SKData(IntPtr handle, bool owns) + : base(handle, owns) + { + } + + // ReferenceNative/UnreferenceNative use type-specific functions + void ISKNonVirtualReferenceCounted.ReferenceNative() => SkiaApi.sk_data_ref(Handle); + void ISKNonVirtualReferenceCounted.UnreferenceNative() => SkiaApi.sk_data_unref(Handle); +} +``` + +**Disposal Logic:** +```csharp +// In SKObject.cs +protected override void DisposeNative() +{ + if (this is ISKReferenceCounted refcnt) + refcnt.SafeUnRef(); // Calls unref (decrements ref count) +} + +// Reference counting extensions +internal static class SKObjectExtensions +{ + public static void SafeRef(this ISKReferenceCounted obj) + { + if (obj is ISKNonVirtualReferenceCounted nvrefcnt) + nvrefcnt.ReferenceNative(); // Type-specific ref + else + SkiaApi.sk_refcnt_safe_ref(obj.Handle); // Virtual ref + } + + public static void SafeUnRef(this ISKReferenceCounted obj) + { + if (obj is ISKNonVirtualReferenceCounted nvrefcnt) + nvrefcnt.UnreferenceNative(); // Type-specific unref + else + SkiaApi.sk_refcnt_safe_unref(obj.Handle); // Virtual unref + } +} +``` + +**Why two interfaces:** +- `SkRefCnt` types use virtual `ref()`/`unref()` - can call through base pointer +- `SkNVRefCnt` types use non-virtual ref/unref - need type-specific function names +- C API exposes `sk_data_ref()`, `sk_textblob_ref()`, etc. for non-virtual types +- C API exposes `sk_refcnt_safe_ref()` for all virtual ref-counted types + +**Handle dictionary behavior:** +```csharp +// GetOrAddObject ensures only one C# wrapper per native handle +internal static TSkiaObject GetOrAddObject(IntPtr handle, bool owns, ...) +{ + if (HandleDictionary has existing wrapper for handle) + { + if (owns && existing is ISKReferenceCounted) + existing.SafeUnRef(); // New reference not needed, unref it + return existing; + } + else + { + var newObject = objectFactory(handle, owns); + RegisterHandle(handle, newObject); + return newObject; + } +} +``` + +**When to use:** +- Immutable objects that can be shared +- Objects with expensive creation/copying +- Objects that may outlive their creator + +**Common types using SkRefCnt (virtual ref-counting):** +- `SKImage` - Immutable images +- `SKShader` - Immutable shaders +- `SKColorFilter` - Immutable color filters +- `SKImageFilter` - Immutable image filters +- `SKTypeface` - Font faces +- `SKPicture` - Recorded drawing commands +- `SKPathEffect` - Path effects +- `SKMaskFilter` - Mask filters +- `SKBlender` - Blend modes +- `SKSurface` - Drawing surfaces + +**Common types using SkNVRefCnt (non-virtual ref-counting):** +- `SKData` - Immutable data blobs (most frequently used ref-counted type) +- `SKTextBlob` - Positioned text runs +- `SKVertices` - Vertex data for custom meshes +- `SKColorSpace` - Color space definitions + +**Difference in usage:** +- Both use ref/unref semantics identically +- C# wrappers use different interfaces but behave the same +- Non-virtual types are lighter weight (4 bytes vs 8-16 bytes overhead) +- Virtual types support polymorphism and inheritance + +## Identifying Pointer Types from C++ Signatures + +### How to Determine Pointer Type + +When adding new API bindings, examine the C++ signature: + +#### Raw Pointer (Non-Owning) +```cpp +// Const reference parameter - borrowed +void draw(const SkPaint& paint); + +// Raw pointer return - caller doesn't own +SkCanvas* getSurface(); + +// Pointer parameter marked as borrowed in docs +void setShader(SkShader* shader); // usually means "borrowed" +``` + +#### Owned Pointer +```cpp +// Mutable classes: Canvas, Paint, Path, Bitmap +SkCanvas* canvas = new SkCanvas(...); + +// Stack allocation +SkPaint paint; + +// Usually indicated by create/new functions +static SkCanvas* Make(...); +``` + +#### Reference-Counted Pointer (Virtual - SkRefCnt) +```cpp +// Inherits from SkRefCnt (has virtual destructor) +class SkImage : public SkRefCnt { + virtual ~SkImage() { } +}; + +// Uses sk_sp smart pointer +sk_sp makeImage(); + +// Returns sk_sp in documentation +static sk_sp MakeFromBitmap(...); + +// Most immutable types (Image, Shader, ColorFilter, etc.) +``` + +#### Reference-Counted Pointer (Non-Virtual - SkNVRefCnt) +```cpp +// Inherits from SkNVRefCnt (no virtual destructor) +class SK_API SkData final : public SkNVRefCnt { }; + +// Uses sk_sp smart pointer (same as SkRefCnt) +sk_sp makeData(); + +// Usually marked as 'final' (cannot inherit) +static sk_sp MakeFromMalloc(...); + +// Lightweight immutable types: Data, TextBlob, Vertices, ColorSpace +``` + +**Rule of thumb:** +- If it inherits `SkRefCnt` or `SkRefCntBase` → Virtual reference counted +- If it inherits `SkNVRefCnt` → Non-virtual reference counted +- If it's mutable (Canvas, Paint, Path) → Owned pointer +- If it's a parameter or returned from getter → Raw pointer (non-owning) + +**How to tell SkRefCnt vs SkNVRefCnt:** +- Check class declaration in C++ header +- `SkNVRefCnt` types are usually marked `final` +- `SkNVRefCnt` types don't have virtual functions (lighter weight) +- Both use same `sk_sp` and ref/unref pattern +- In C# layer: `ISKReferenceCounted` vs `ISKNonVirtualReferenceCounted` + +## Common Mistakes and How to Avoid Them + +### Mistake 1: Treating Reference-Counted as Owned + +**Wrong:** +```cpp +SK_C_API void sk_image_destroy(sk_image_t* image) { + delete AsImage(image); // WRONG! Images are ref-counted +} +``` + +**Correct:** +```cpp +SK_C_API void sk_image_unref(const sk_image_t* image) { + SkSafeUnref(AsImage(image)); // Correct: decrement ref count +} +``` + +### Mistake 2: Not Incrementing Ref Count When Storing + +**Wrong:** +```cpp +// C++ expects sk_sp, which would increment ref +SK_C_API sk_image_t* sk_image_new_raster_data(..., sk_data_t* pixels, ...) { + return ToImage(SkImages::RasterFromData(..., AsData(pixels), ...).release()); + // WRONG: AsData(pixels) creates raw pointer, no ref increment +} +``` + +**Correct:** +```cpp +SK_C_API sk_image_t* sk_image_new_raster_data(..., sk_data_t* pixels, ...) { + return ToImage(SkImages::RasterFromData(..., sk_ref_sp(AsData(pixels)), ...).release()); + // Correct: sk_ref_sp increments ref count +} +``` + +### Mistake 3: Double-Freeing with Wrong Ownership + +**Wrong:** +```csharp +public class SKImage : SKObject +{ + // Created owned wrapper but image is ref-counted + public static SKImage FromBitmap(SKBitmap bitmap) + { + var handle = SkiaApi.sk_image_new_from_bitmap(bitmap.Handle); + return new SKImage(handle, true); // Will call delete instead of unref + } +} +``` + +**Correct:** +```csharp +public class SKImage : SKObject, ISKReferenceCounted // Implement ISKReferenceCounted +{ + public static SKImage FromBitmap(SKBitmap bitmap) + { + var handle = SkiaApi.sk_image_new_from_bitmap(bitmap.Handle); + return GetObject(handle); // ISKReferenceCounted triggers unref on dispose + } +} +``` + +### Mistake 4: Disposing Borrowed Objects + +**Wrong:** +```csharp +public SKSurface Surface +{ + get { + var handle = SkiaApi.sk_get_surface(Handle); + return new SKSurface(handle, true); // WRONG: will destroy surface owned by canvas + } +} +``` + +**Correct:** +```csharp +public SKSurface Surface +{ + get { + var handle = SkiaApi.sk_get_surface(Handle); + return SKObject.GetOrAddObject(handle, owns: false, // Correct: non-owning wrapper + (h, o) => new SKSurface(h, o)); + } +} +``` + +## Memory Lifecycle Patterns + +### Pattern 1: Create and Dispose (Owned) + +```csharp +using (var paint = new SKPaint()) // Creates owned object +{ + paint.Color = SKColors.Red; + canvas.DrawRect(rect, paint); +} // Dispose calls sk_paint_delete +``` + +### Pattern 2: Factory and Ref Counting (Reference-Counted) + +```csharp +SKImage image = SKImage.FromBitmap(bitmap); // ref count = 1 +SKImage image2 = image; // Both variables reference same native object +// No ref count increment in C# (handle dictionary ensures single wrapper) + +image.Dispose(); // ref count still >= 1 (wrapper disposed but object alive) +image2.Dispose(); // Now ref count decremented, possibly deleted +``` + +### Pattern 3: Ownership Transfer + +```csharp +var drawable = new SKDrawable(); +canvas.DrawDrawable(drawable); // Canvas takes ownership +// drawable.RevokeOwnership() called internally +// drawable wrapper still exists but won't dispose native object +``` + +### Pattern 4: Parent-Child Relationships + +```csharp +var surface = SKSurface.Create(info); // Parent owns surface +var canvas = surface.Canvas; // Child owned by parent (non-owning wrapper) + +canvas.DrawRect(...); // Safe to use +surface.Dispose(); // Destroys surface AND canvas +// canvas wrapper still exists but native object is gone - don't use! +``` + +## Thread Safety Considerations + +### Reference Counting +- Reference count increments/decrements are atomic (thread-safe) +- Creating/destroying objects from multiple threads is safe +- Using the same object from multiple threads is NOT safe + +### Handle Dictionary +- Backed by a shared `Dictionary` protected by a reader/writer lock +- Multiple threads can safely request wrappers, but calls run inside the lock to keep state consistent +- Don't access disposed objects from any thread + +### Best Practices +1. Create objects on one thread, use on same thread +2. Don't share mutable objects (SKCanvas, SKPaint) across threads +3. Immutable objects (SKImage) can be shared after creation +4. Always dispose on the same thread that uses the object + +## Summary Table + +| Pointer Type | C++ | C API | C# | Cleanup | Example Types | +|--------------|-----|-------|-----|---------|---------------| +| **Raw (Non-Owning)** | `SkType*` | `sk_type_t*` | `OwnsHandle=false` | None (owned elsewhere) | Parameters, getters | +| **Owned** | `new SkType()` | `sk_type_new/delete` | `OwnsHandle=true`, no ref counting | `delete` or `destroy` | SKCanvas, SKPaint, SKPath | +| **Reference-Counted** | `sk_sp`, `SkRefCnt` | `sk_type_ref/unref` | `ISKReferenceCounted` | `unref()` | SKImage, SKShader, SKData | + +## Next Steps + +- See [Error Handling](error-handling.md) for how errors are managed across pointer types +- See [Adding New APIs](adding-new-apis.md) for step-by-step guide using correct pointer types