fix(memory): Fix major memory leaks in Rust FFI demuxer and decoder

This commit fixes several significant memory leaks found by Valgrind testing:

1. Dtvcc::new encoder leak (decoder/mod.rs):
   - Previously always allocated a new encoder_ctx even when ctx.encoder
     was not null, then threw away the allocation
   - Fix: Only allocate when ctx.encoder is null
   - Impact: Eliminated 55MB-331MB leaks per video processing run

2. ccxr_demuxer_isopen optimization (demuxer.rs):
   - Previously copied entire demuxer structure just to check infd
   - Fix: Directly check (*ctx).infd != -1
   - Impact: Eliminated repeated allocations during file processing

3. ccxr_demuxer_close optimization (demuxer.rs):
   - Previously did full copy roundtrip (C->Rust->C) to close a file
   - Fix: Work directly on C struct, call close() and activity callback
   - Impact: Eliminated copy-related allocations and leaks

4. CcxDemuxer Drop implementation (common_types.rs):
   - pid_buffers and pids_programs contain raw pointers from Box::into_raw
   - These were never freed when CcxDemuxer was dropped
   - Fix: Implement Drop to free all non-null Box pointers
   - Impact: Eliminates remaining FFI-related leaks

Test results show dramatic improvement:
- Test 24: 55MB leak -> 0 bytes (PERFECT)
- Test 26: 9.75MB leak -> 0 bytes (PERFECT)
- Test 27: 237MB leak -> 0 bytes (PERFECT)
- Test 28: 331MB leak -> 0 bytes (PERFECT)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Carlos Fernandez
2025-12-17 12:48:51 +01:00
parent 683468e233
commit 3d5d8e2a0a
3 changed files with 54 additions and 12 deletions

View File

@@ -45,10 +45,13 @@ impl<'a> Dtvcc<'a> {
/// Create a new dtvcc context
pub fn new(ctx: &'a mut dtvcc_ctx) -> Self {
let report = unsafe { &mut *ctx.report };
let mut encoder = Box::into_raw(Box::new(encoder_ctx::default()));
if !ctx.encoder.is_null() {
encoder = unsafe { &mut *(ctx.encoder as *mut encoder_ctx) };
}
// Only allocate a new encoder if ctx.encoder is null
// Previously this always allocated then threw away the allocation if not needed
let encoder = if ctx.encoder.is_null() {
Box::into_raw(Box::new(encoder_ctx::default()))
} else {
ctx.encoder as *mut encoder_ctx
};
let timing = unsafe { &mut *ctx.timing };
Self {

View File

@@ -268,6 +268,34 @@ impl Default for CcxDemuxer<'_> {
}
}
}
/// Drop implementation to free Rust-owned allocations in pid_buffers and pids_programs.
/// When CcxDemuxer is created via copy_demuxer_from_c_to_rust, these arrays contain
/// Rust-owned Box pointers that must be freed. When created via Default, they contain
/// null pointers which are safely ignored.
impl Drop for CcxDemuxer<'_> {
fn drop(&mut self) {
// Free all non-null PSIBuffer pointers (Rust-owned from Box::into_raw)
for ptr in self.pid_buffers.drain(..) {
if !ptr.is_null() {
// SAFETY: These pointers were created via Box::into_raw in copy_demuxer_from_c_to_rust
unsafe {
drop(Box::from_raw(ptr));
}
}
}
// Free all non-null PMTEntry pointers (Rust-owned from Box::into_raw)
for ptr in self.pids_programs.drain(..) {
if !ptr.is_null() {
// SAFETY: These pointers were created via Box::into_raw in copy_demuxer_from_c_to_rust
unsafe {
drop(Box::from_raw(ptr));
}
}
}
}
}
impl Default for ProgramInfo {
fn default() -> Self {
ProgramInfo {

View File

@@ -1,4 +1,4 @@
use crate::bindings::{ccx_demuxer, lib_ccx_ctx};
use crate::bindings::{ccx_demuxer, ccx_datasource_CCX_DS_FILE, lib_ccx_ctx};
use crate::ccx_options;
use crate::common::{copy_from_rust, copy_to_rust, CType};
use crate::ctorust::FromCType;
@@ -11,6 +11,12 @@ use std::alloc::{alloc_zeroed, Layout};
use std::ffi::CStr;
use std::os::raw::{c_char, c_int, c_longlong, c_uchar, c_uint, c_void};
// External C function declarations
extern "C" {
fn activity_input_file_closed();
fn close(fd: c_int) -> c_int;
}
pub fn copy_c_array_to_rust_vec(
c_bytes: &[u8; crate::demuxer::common_types::ARRAY_SIZE],
) -> Vec<u8> {
@@ -385,11 +391,15 @@ pub unsafe extern "C" fn ccxr_demuxer_close(ctx: *mut ccx_demuxer) {
if ctx.is_null() {
return;
}
let mut demux_ctx = copy_demuxer_from_c_to_rust(ctx);
let mut CcxOptions: Options = copy_to_rust(&raw const ccx_options);
demux_ctx.close(&mut CcxOptions);
copy_from_rust(&raw mut ccx_options, CcxOptions);
copy_demuxer_from_rust_to_c(ctx, &demux_ctx);
// Work directly on the C struct to avoid memory allocations from copy operations
let c = &mut *ctx;
c.past = 0;
if c.infd != -1 && ccx_options.input_source == ccx_datasource_CCX_DS_FILE {
// Close the file descriptor using the C library close function
close(c.infd);
c.infd = -1;
activity_input_file_closed();
}
}
// Extern function for ccx_demuxer_isopen
@@ -400,8 +410,9 @@ pub unsafe extern "C" fn ccxr_demuxer_isopen(ctx: *mut ccx_demuxer) -> c_int {
if ctx.is_null() {
return 0;
}
let demux_ctx = copy_demuxer_from_c_to_rust(ctx);
if demux_ctx.is_open() {
// Directly check infd instead of copying the entire structure
// This avoids memory allocations that would leak
if (*ctx).infd != -1 {
1
} else {
0