Skip to content

URI Path Parsing Bug in Generated FMU C++ Code - Missing Leading Slash on Unix #109

Description

@avullo

Summary

MLFMU generates FMUs with a C++ path parsing bug that causes FMU instantiation to fail on Unix systems when using absolute paths. The bug is in the formatOnnxPath function which incorrectly strips URI prefixes.

Root Cause

In mlfmu/fmu_build/templates/onnx_fmu/onnxFmu.cpp, the formatOnnxPath function has incorrect substring logic around lines 44-46:

// BUGGY CODE
std::wstring startPath = path.substr(0, 8);        // "file:///"
std::wstring endPath = path.substr(8);             // strips 8 chars - TOO MANY!
if (startPath == L"file:///") {
    path = endPath;  // Results in "tmp/path" instead of "/tmp/path"
}

Problem Description

When fmpy (and other FMU libraries) pass absolute paths to the FMU, they convert them to file:// URIs:

  • Input: file:///tmp/extracted_fmu/resources/model.onnx
  • Expected output: /tmp/extracted_fmu/resources/model.onnx
  • Actual output: tmp/extracted_fmu/resources/model.onnx ❌ (missing leading /)

This causes ONNX model loading to fail with:

[ERROR] Load model from tmp/extracted_dir/resources/model.onnx failed: File doesn't exist

Reproduction Case

#!/usr/bin/env python3

"""
This script demonstrates the C++ path parsing bug in MLFMU-generated FMUs
where 'file:///' URI prefixes are incorrectly stripped, causing missing
leading slashes in absolute paths on Unix systems.
"""

import tempfile
from pathlib import Path
import json
import onnx
import onnx.helper as helper
from mlfmu.api import MlFmuBuilder

def create_simple_onnx_model(onnx_path: Path) -> None:
    """Create a minimal valid ONNX model for testing."""
    
    # Create simple model: 2 inputs -> 1 output (just sum the inputs)
    input_tensor = helper.make_tensor_value_info('input', onnx.TensorProto.FLOAT, [1, 2])
    output_tensor = helper.make_tensor_value_info('output', onnx.TensorProto.FLOAT, [1, 1])
    
    # Sum operation: output = input[0] + input[1]
    node = helper.make_node(
        'ReduceSum',
        inputs=['input'],
        outputs=['output'],
        axes=[1]  # Sum along the feature dimension
    )
    
    # Create graph and model with opset 11 for compatibility
    graph = helper.make_graph([node], "simple_graph", [input_tensor], [output_tensor])
    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 11)])
    
    # Save ONNX model
    onnx.save_model(model, str(onnx_path))


def create_simple_mlfmu_interface(interface_path: Path) -> None:
    """Create a minimal MLFMU interface configuration."""
    
    interface = {
        "name": "SimpleBugDemo",
        "inputs": [
            {"name": "A", "agentInputIndexes": ["0"]}, 
            {"name": "B", "agentInputIndexes": ["1"]}
        ],
        "outputs": [
            {"name": "C", "agentOutputIndexes": ["0"]}
        ]
    }
    
    with open(interface_path, 'w') as f:
        json.dump(interface, f, indent=2)


def test_fmu_instantiation(fmu_path: Path) -> None:
    """Test FMU instantiation using fmpy (this triggers the path bug)."""
    
    import fmpy
    from fmpy import read_model_description
    from fmpy.fmi2 import FMU2Slave
    
    print("🔍 Testing FMU instantiation (where path bug occurs)...")
    
    # Load model description
    model_description = read_model_description(str(fmu_path))
    print(f"✅ Model description loaded: {model_description.modelName}")
    
    # Extract FMU to temporary directory
    with tempfile.TemporaryDirectory() as temp_extract_dir:
        extracted_dir = fmpy.extract(str(fmu_path), temp_extract_dir)
        print(f"✅ FMU extracted to: {extracted_dir}")
        
        print("🚨 Creating FMU2Slave with absolute path (triggers C++ path bug)...")
        
        # Initialize FMU slave - this will work
        fmu_slave = FMU2Slave(
            guid=model_description.guid,
            unzipDirectory=extracted_dir,  # This is an absolute path like /tmp/xxx
            modelIdentifier=model_description.coSimulation.modelIdentifier,
            instanceName="test_bug_demo",
        )
        print("✅ FMU2Slave created")
        
        print("🎯 Calling fmu_slave.instantiate() - this will trigger the C++ path bug...")
        
        try:
            # This is where the C++ path parsing bug occurs
            # The FMU's C++ code will try to load the ONNX model using a path like:
            # "file:///tmp/extracted_dir/resources/simple_model.onnx"
            # But the buggy formatOnnxPath() strips 8 characters, leaving:
            # "tmp/extracted_dir/resources/simple_model.onnx" (missing leading slash)
            fmu_slave.instantiate()
            print("🎉 FMU instantiation succeeded! (Bug may be fixed)")
            
            # Clean up if successful
            try:
                fmu_slave.terminate()
                fmu_slave.freeInstance()
            except:
                pass
                
        except Exception as instantiation_error:
            print(f"❌ FMU instantiation failed: {instantiation_error}")
            
            # Check if this is the expected path-related error
            error_str = str(instantiation_error).lower()
            if "file doesn't exist" in error_str or "load model" in error_str:
                print("✅ This is the expected MLFMU C++ path parsing bug!")
                print("   The ONNX model path is missing its leading slash due to")
                print("   incorrect URI parsing in mlfmu/fmu_build/templates/onnx_fmu/onnxFmu.cpp")
            else:
                print("Different error occurred:", instantiation_error)


def create_minimal_reproduction():
    """Main function to reproduce the MLFMU path bug."""
    
    with tempfile.TemporaryDirectory() as temp_dir:
        temp_path = Path(temp_dir)
        print(f"Working directory: {temp_path}")
        print(f"Absolute path: {temp_path.resolve()}")
        print()
        
        # Step 1: Create ONNX model
        print("📝 Creating ONNX model...")
        onnx_path = temp_path / "simple_model.onnx"
        create_simple_onnx_model(onnx_path)
        print(f"✅ ONNX model created: {onnx_path}")
        print()
        
        # Step 2: Create MLFMU interface
        print("📋 Creating MLFMU interface...")
        interface_path = temp_path / "interface.json"
        create_simple_mlfmu_interface(interface_path)
        print(f"✅ Interface created: {interface_path}")
        print()
        
        # Step 3: Build FMU with MLFMU
        print("🔨 Building FMU with MLFMU...")
        
        builder = MlFmuBuilder(
            onnx_path=str(onnx_path),
            interface_path=str(interface_path),
            fmu_output_folder=str(temp_path.resolve()),  # Use absolute paths
            root_directory=str(temp_path.resolve())
        )
        
        try:
            builder.generate()
            print("✅ FMU generation completed")
            
            builder.compile()
            print("✅ FMU compilation completed")
            
            builder.build()
            print("✅ FMU build completed")
            
        except Exception as e:
            print(f"❌ FMU build failed: {e}")
            return
        
        # Step 4: Find the generated FMU
        # MLFMU may create the FMU in different locations depending on the configuration
        fmu_files = []
        
        # Check temp directory first
        fmu_files = list(temp_path.rglob("*.fmu"))
        
        # If not found, check current working directory (common for MLFMU)
        if not fmu_files:
            fmu_files = list(Path.cwd().glob("*.fmu"))
            if fmu_files:
                print(f"📂 Found FMU in current directory: {[f.name for f in fmu_files]}")
        
        if not fmu_files:
            print("❌ No FMU file found after build")
            return
        
        fmu_file = fmu_files[0]
        print(f"✅ FMU created: {fmu_file}")
        print()
        
        # Step 5: Test FMU instantiation (this will trigger the path bug)
        test_fmu_instantiation(fmu_file)


if __name__ == "__main__":
    print("🧪 MLFMU Path Bug Reproduction")
    print("=" * 50)
    create_minimal_reproduction()

Expected vs Actual Behavior

Input URI Expected Path Actual Path Status
file:///tmp/path/model.onnx /tmp/path/model.onnx tmp/path/model.onnx ❌ Missing leading /
file:///home/user/model.onnx /home/user/model.onnx home/user/model.onnx ❌ Missing leading /

Impact

  • Severity: High - FMUs cannot be instantiated on Unix systems with absolute paths
  • Scope: Affects all FMUs generated by MLFMU when used with standard FMU libraries (fmpy, PyFMI, etc.)
  • Workaround: None available - the bug is in compiled C++ code within the FMU binary

Proposed Fix

Option 1: Simple Fix

// In mlfmu/fmu_build/templates/onnx_fmu/onnxFmu.cpp
// Replace lines ~44-46:

// CURRENT (BUGGY)
std::wstring startPath = path.substr(0, 8);        // "file:///"  
std::wstring endPath = path.substr(8);             // strips 8 chars
if (startPath == L"file:///") {
    path = endPath;
}

// FIXED 
std::wstring startPath = path.substr(0, 7);        // "file://"
std::wstring endPath = path.substr(7);             // strips 7 chars  
if (startPath == L"file://") {
    path = endPath;
}

Option 2: Robust Fix

// Handle both file:// and file:/// prefixes correctly
if (path.length() >= 7 && path.substr(0, 7) == L"file://") {
    // Always strip exactly 7 characters for "file://"
    path = path.substr(7);
}

Environment

  • OS: Linux (Ubuntu 22.04, similar Unix systems)
  • MLFMU version: 1.0.3
  • Python: 3.10+
  • fmpy: 0.3.22
  • ONNX: 1.17.0

Additional Notes

This bug specifically affects Unix systems where absolute paths start with /. Windows systems using drive letters (e.g., C:\path) may not be affected due to different URI formatting.

The bug occurs because:

  1. fmpy converts absolute paths to file:// URIs as per standard
  2. MLFMU's C++ code assumes all file:// URIs have 3 slashes (file:///)
  3. The substring logic strips 8 characters instead of 7
  4. Result: missing leading slash in absolute paths

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions