refactor(image-decoder): adopt RAII for FFmpeg resources#7
refactor(image-decoder): adopt RAII for FFmpeg resources#7
Conversation
- Add MemoryBufferContextDeleter for custom image decoder context - Add AVIOContextDeleter with automatic buffer cleanup - Add ImageFormatContextDeleter for input format contexts - Add unit test verifying deleter behavior These RAII wrappers will be used in Task 2 to refactor image_decoder.cc to use automatic resource management instead of manual cleanup.
- Replace raw AVFormatContext* with ImageFormatContextPtr - Replace raw AVIOContext* with AVIOContextPtr - Replace raw MemoryBufferContext* with MemoryBufferContextPtr - Prepares for automatic cleanup in image_decoder.cc - Add test for multiple decoder construction without leaks
- Migrate ParseAnimatedImageMetadata to use RAII wrappers - Remove 8+ duplicate cleanup sequences in error paths - Simplify Cleanup() method (automatic resource management) - Move MemoryBufferContext to ffmpeg namespace for RAII compatibility - Fix: memory leaks in error paths now impossible
- Add instance counter for ImageDecoder in common.cc/common.h - Export counter to JavaScript via addon.cc - Add stress tests for animated GIF and static PNG decode - Update test helper to check ImageDecoder count - Use IIFE wrapping for proper GC behavior in tests - Validates RAII refactoring prevents memory leaks
PR Review: RAII Adoption for ImageDecoderThank you for this excellent refactoring! This PR demonstrates strong understanding of C++ RAII patterns and FFmpeg resource management. Here's my detailed review following the repository's code review standards. ✅ Strengths1. Excellent RAII Implementation
2. Memory Leak Prevention
3. Test Coverage Excellence
4. Clean Code Organization
🔍 Issues & RecommendationsCRITICAL: Resource Cleanup Order IssueLocation: src/image_decoder.cc:254-259 The comment states "format_context_ first (may reference avio_context_->buffer)" but this is incorrect based on FFmpeg semantics: // Reset animated image RAII members
// Order matters: format_context_ first (may reference avio_context_->buffer)
// then avio_context_ (frees buffer too via AVIOContextDeleter)
// then mem_ctx_ (no longer needed)
format_context_.reset(); // Calls ImageFormatContextDeleter
avio_context_.reset(); // Calls AVIOContextDeleter (frees buffer too)
mem_ctx_.reset(); // Calls MemoryBufferContextDeleterProblem:
Since we used a custom AVIO context (not However, the current order happens to work because:
Risk: If FFmpeg's implementation changes to access Recommendation: The safest order is: // Reset in reverse construction order for safety:
// 1. format_context_ (may read from avio_context_->buffer during close)
// 2. avio_context_ (frees buffer allocated with av_malloc)
// 3. mem_ctx_ (opaque pointer no longer needed)
format_context_.reset();
avio_context_.reset();
mem_ctx_.reset();The current order is correct, but the comment should be clarified: // Reset animated image RAII members in reverse construction order
// 1. format_context_ first (avformat_close_input may read from pb but doesn't free it)
// 2. avio_context_ second (we own this custom context, must free it ourselves)
// 3. mem_ctx_ last (opaque pointer used by avio_context_)Minor: Potential Double-Free in AVIOContextDeleterLocation: src/ffmpeg_raii.h:129-139 struct AVIOContextDeleter {
void operator()(AVIOContext* ctx) const noexcept {
if (ctx) {
// Free the buffer allocated with av_malloc before freeing context
if (ctx->buffer) {
av_freep(&ctx->buffer); // ⚠️ Potential issue
}
avio_context_free(&ctx);
}
}
};Issue: According to FFmpeg source, Recommendation: struct AVIOContextDeleter {
void operator()(AVIOContext* ctx) const noexcept {
if (ctx) {
// avio_context_free handles buffer cleanup internally
avio_context_free(&ctx);
}
}
};However, verify this with your FFmpeg version. If you're targeting older FFmpeg versions that don't auto-free the buffer, keep the explicit free. Action: Add a comment documenting which FFmpeg versions need explicit buffer freeing, or test with minimum supported version (FFmpeg 5.0 per CLAUDE.md). Code Style: Inconsistent RAII Ordering in Cleanup()Location: src/image_decoder.cc:246-259 The cleanup order for static vs animated resources seems arbitrary: void ImageDecoder::Cleanup() {
// Reset RAII members (automatic cleanup)
codec_context_.reset(); // Static image resources
sws_context_.reset();
frame_.reset();
packet_.reset();
// Reset animated image RAII members
format_context_.reset(); // Animated image resources
avio_context_.reset();
mem_ctx_.reset();
// ...
}Recommendation: Add comments explaining why static resources are reset first, or group by dependency order: void ImageDecoder::Cleanup() {
// Reset codec resources (used by both static and animated paths)
codec_context_.reset();
sws_context_.reset();
frame_.reset();
packet_.reset();
// Reset animated image format/IO resources (in reverse construction order)
format_context_.reset(); // Uses avio_context_ during close
avio_context_.reset(); // Uses mem_ctx_ as opaque pointer
mem_ctx_.reset(); // No dependencies
// Clear remaining state
video_stream_index_ = -1;
decoded_data_.clear();
decoded_frames_.clear();
}Testing: Missing Error Path CoverageLocation: test/stress/memory-leak.test.ts The stress tests only cover success paths:
Missing: Tests for error paths where RAII cleanup is critical: it('ImageDecoder: RAII cleanup on invalid data', async () => {
const before = getCounters();
for (let i = 0; i < 50; i++) {
await (async () => {
const { ImageDecoder } = await import('@pproenca/node-webcodecs');
let decoder: InstanceType<typeof ImageDecoder> | null = null;
try {
// Provide invalid GIF data to trigger error paths
decoder = new ImageDecoder({
type: 'image/gif',
data: new Uint8Array([0x47, 0x49, 0x46]), // Truncated header
});
// This should throw due to invalid data
await decoder.decode({ frameIndex: 0 });
} catch (e) {
// Expected error
} finally {
if (decoder) {
decoder.close();
decoder = null;
}
}
})();
}
// Force GC
for (let i = 0; i < 5; i++) {
forceGC();
await new Promise(resolve => setTimeout(resolve, 100));
}
const after = getCounters();
assertNoLeaks(before, after, 'ImageDecoder error path');
});Rationale: RAII's main benefit is preventing leaks in error paths. The current tests don't verify this critical behavior. Documentation: Missing FFmpeg Upstream ReferencesLocation: src/ffmpeg_raii.h:127-150 The new deleters lack references to FFmpeg documentation. Following CLAUDE.md guidelines:
Recommendation: Add upstream references: // AVIOContext deleter (handles avio_context_free semantics)
// FFmpeg docs: https://ffmpeg.org/doxygen/trunk/group__lavf__io.html#ga3d50df9c77f408d2e5022c3e370a5813
// NOTE: avio_context_free auto-frees ctx->buffer in FFmpeg 5.0+
// For custom AVIO contexts, avformat_close_input does NOT free the context
struct AVIOContextDeleter {
void operator()(AVIOContext* ctx) const noexcept {
if (ctx) {
avio_context_free(&ctx); // Frees both context and buffer
}
}
};📝 Nitpicks (Non-blocking)
🎯 Action Items (Before Merge)MUST FIX:
SHOULD FIX:
CONSIDER:
✅ Review Checklist (Per CLAUDE.md)
🎉 ConclusionOverall Assessment: Excellent work! This PR significantly improves code safety and maintainability. Recommendation: Approve with minor changes The RAII refactoring is well-executed with proper ownership semantics and comprehensive testing. The identified issues are minor (mostly documentation/comments), but should be addressed to prevent future maintenance headaches. Specific Requests:
Once these are addressed, this PR is ready to merge. Great contribution to the project's safety and maintainability! Reviewed by: Claude Code Agent |
|
Closing PR as requested |
Summary
Complete RAII adoption for ImageDecoder's FFmpeg resource management:
Benefits
Files Changed (11)
src/ffmpeg_raii.h- New deleters for ImageDecoder-specific typessrc/image_decoder.cc- Converted to RAII memberstest/stress/memory-leak.test.ts- Added leak detection testTest Plan
Commits