Skip to content

Testing of C# <-> Rust FFI #12

@patdhlk

Description

@patdhlk

Testing of C# <-> FFI Interface

(Code) Example Of Cumbersome API

The current FFI testing approach has several pain points:

  1. No systematic error path testing - Tests only validate success paths, leaving FFI error codes and error translation
    untested.

  2. Union marshaling requires manual workarounds - Complex union types like iox2_static_config_t cannot be properly
    marshaled, requiring manual memory operations.

  3. Handle lifecycle edge cases are untested - No tests for double-disposal, use-after-free detection, or stress testing
    handle creation/cleanup cycles.

  4. Callback testing is limited and not thread-safe - Service discovery uses thread-local state (_tempServiceList) which is
    error-prone.

// Current approach: Only success paths tested, no FFI error validation                                                         
[Fact]                                                                                                                          
public void NativeLibraryLoads()                                                                                                
{                                                                                                                               
    var exception = Record.Exception(() =>                                                                                      
    {                                                                                                                           
        Iox2NativeMethods.iox2_set_log_level_from_env_or(                                                                       
            Iox2NativeMethods.iox2_log_level_e.INFO);                                                                           
    });                                                                                                                         
    Assert.Null(exception);  // No error code validation                                                                        
}                                                                                                                               
                                                                                                                                
// Current workaround for union marshaling limitations                                                                          
// Note: Due to the simplified constructor (to avoid union marshaling issues),                                                  
// PublishSubscribeConfig will be null. We can only verify the service list works.                                              
// Pattern-specific configs will be null until we solve the union marshaling problem.                                           
                                                                                                                                
// Current handle passing pattern - unintuitive and error-prone                                                                 
var publisherHandle = _handle.DangerousGetHandle();                                                                             
var result = Native.Iox2NativeMethods.iox2_publisher_loan_slice_uninit(                                                         
    ref publisherHandle,  // Pass by reference for C to modify                                                                  
    IntPtr.Zero,                                                                                                                
    out var sampleHandle,                                                                                                       
    (UIntPtr)1);                                                                                                                
// No validation of error codes beyond IOX2_OK  

Improvement Suggestion

Introduce a comprehensive FFI testing framework that:

  1. Validates all FFI error codes - Test each error path and ensure proper translation to C# error types.
  2. Tests handle lifecycle robustly - Include double-disposal detection, use-after-free scenarios, and concurrent handle
    operations.
  3. Provides thread-safe callback testing utilities - Replace thread-local state with proper synchronization mechanisms.
  4. Documents struct alignment validation - Add compile-time or runtime checks for struct size/alignment mismatches.
  // Proposed: Comprehensive FFI error testing                                                                                    
  [Theory]                                                                                                                        
  [InlineData(iox2_service_builder_open_error_e.DOES_NOT_EXIST)]                                                                  
  [InlineData(iox2_service_builder_open_error_e.INSUFFICIENT_PERMISSIONS)]                                                        
  [InlineData(iox2_service_builder_open_error_e.INTERNAL_FAILURE)]                                                                
  public void FfiErrorCodes_AreProperlyTranslated(iox2_service_builder_open_error_e errorCode)                                    
  {                                                                                                                               
      var csharpError = Iox2ErrorTranslator.Translate(errorCode);                                                                 
      Assert.NotEqual(Iox2Error.Unknown, csharpError);                                                                            
      Assert.NotNull(csharpError.Message);                                                                                        
  }                                                                                                                               
                                                                                                                                  
  // Proposed: Handle lifecycle tests                                                                                             
  [Fact]                                                                                                                          
  public void SafeHandleDoubleDisposeDoesNotThrow()                                                                             
  {                                                                                                                               
      var handle = CreateTestHandle();                                                                                            
      handle.Dispose();                                                                                                           
      var exception = Record.Exception(() => handle.Dispose());                                                                   
      Assert.Null(exception);                                                                                                     
  }                                                                                                                               
                                                                                                                                  
  [Fact]                                                                                                                          
  public void SafeHandleUseAfterDisposeThrowsObjectDisposedException()                                                          
  {                                                                                                                               
      var publisher = CreateTestPublisher();                                                                                      
      publisher.Dispose();                                                                                                        
      Assert.Throws<ObjectDisposedException>(() => publisher.Loan<TestData>());                                                   
  }                                                                                                                               
                                                                                                                                  
  // Proposed: Thread-safe callback testing                                                                                       
  [Fact]                                                                                                                          
  public async Task ServiceDiscoveryConcurrentCallsAreThreadSafe()                                                              
  {                                                                                                                               
      var tasks = Enumerable.Range(0, 10)                                                                                         
          .Select(_ => Task.Run(() => ServiceDiscovery.List()))                                                                   
          .ToArray();                                                                                                             
                                                                                                                                  
      var results = await Task.WhenAll(tasks);                                                                                    
      Assert.All(results, r => Assert.NotNull(r));                                                                                
  }                                                                                                                               
                                                                                                                                  
  // Proposed: Struct alignment validation utility                                                                                
  [Fact]                                                                                                                          
  public void FfiStructsHaveCorrectSize()                                                                                        
  {                                                                                                                               
      // Validate C# struct sizes match C FFI expectations                                                                        
      Assert.Equal(                                                                                                               
          Iox2NativeMethods.iox2_node_builder_storage_t.Size,                                                                     
          FfiStructValidator.GetExpectedSize("iox2_node_builder_storage_t"));                                                     
  } 

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions