Port C++ base class/inherited class code to Rust

I am new to Rust and used to learn programming by writing code for my own little project. I tried to port C++ code to Rust but have not find an appropriate way.

I have different instruments wtih serial port communication. In C++, I created a base class Device which is inherited from CWinThread. The base class Device implemented serial port communication. i.e. Once the object is created, it starts a thread loop.

	while (bLoop)
	{
		// Always check if the port is still open
        if(bOpen != IsPortOpen() || !bValid)
        {
			bOpen = IsPortOpen();
			m_ConnStatus = bOpen? EConnStatusPortOpen : EConnStatusNotConnected;
			NotifyConnectionUpdated();
			if(!bOpen)
				bValid = false;
        }

		int timeout = m_UpdateConnInterval;
		if (bOpen && bValid)
			timeout = m_UpdateDevInterval;

		switch(WaitForMultipleObjectsEx(3, hEventList, false, timeout, true))
		{
		case WAIT_OBJECT_0:
			// Terminate thread
			bLoop = false;
			break;
		case WAIT_OBJECT_0 + 1:
			// Port change, reopen
			bOpen = OpenPort();
			m_ConnStatus = bOpen? EConnStatusPortOpen : EConnStatusNotConnected;
			NotifyConnectionUpdated();
			bValid = false;
			break;
		case WAIT_OBJECT_0 + 2:
			// Send command and wait for response
			{
				CDeviceCommand cmd = ReadFromSendQueue();
				if (!cmd.m_Cmd.IsEmpty())
				{
					if (bOpen)
						cmd.m_Resp = SendCommand(cmd.m_Cmd, cmd.m_Timeout);
					else
						cmd.m_Resp = _T("ERROR: port not open!");
					if (cmd.HasCallback())
						(*cmd.m_Callback)(cmd);
					else
						AddToReceiveQueue(cmd);
				}
			}
			break;
		case WAIT_IO_COMPLETION:
			// This happens when IO is finished
			break;
		default:
			if (!bOpen)
			{
				// Port not open: try to open
				bOpen = OpenPort();
				m_ConnStatus = bOpen? EConnStatusPortOpen : EConnStatusNotConnected;
				NotifyConnectionUpdated();
				bValid = false;
			}
			else if (bValid)
			{
				// Port open, connection valid: update device
				if (UpdateDevice())
				{
					bValid = true;
					m_Retries = 0;
				}
				else if (m_Retries++ == 5)
				{
					// After 5 retries, disconnect is signalled
					bValid = false;
				}
				NotifyDeviceUpdated();
			}
			else
			{
				// Port open, connection not valid: update connection
				bool newValid = UpdateConnection();
				if (bValid != newValid)
				{
					bValid = newValid;
					m_ConnStatus = bValid? EConnStatusConnected : EConnStatusPortOpen;
					NotifyConnectionUpdated();
				}
			}
			break;
		}
	}

The thread continously check and update the serial port connection; if the port is closed or the port name is changed, try to open the port; if there is command in the command queue, send the command to the port and put response to response queue. Call virtual methods to actually handle connection status and unpdate the status in UI.

	virtual void GetPortSettings(SPortSettings *ps) const {}
	virtual bool UpdateDevice() { return false; }
	virtual void NotifyDeviceUpdated() const {}
	virtual bool UpdateConnection() { return false; }
	virtual void NotifyConnectionUpdated() const {}

Each kind of instrument has its own class inherited the base class Device and actually implemented the virtual methods defined in the base class Device and their own method for instrument control.

In Rust, I ended up very ugly implementation with struct and the code is not reusable as C++ since there is no inheritance in Rust. Here is my rust code, and I don't like it at all. How shall I implement such easy feature in C++ in the Rust? Look forward to your guidance.

#[derive(Debug, Clone)]
pub struct Device {
    pub terminator: String,
    pub name: Arc<RwLock<String>>,
    pub baud_rate: u32,
    pub data_bits: DataBits,
    pub flow_control: FlowControl,
    pub parity: Parity,
    pub stop_bits: StopBits,
    pub timeout: Duration,
    port_changed: Arc<RwLock<bool>>,
    connected_lock: Arc<RwLock<bool>>,
    cmd_tx: Sender<String>,
    resp_rx: Arc<Mutex<Receiver<Result<Packet>>>>,   
}

impl Device{
    pub fn new(terminator:&str, ...)>) -> Self{
        ...
        let _thread_handle = thread::spawn(move|| {        
            serial_thread(
                port_name_clone,
                terminator_t,
                ....
                cmd_rx,
                resp_tx,
                port_changed_clone,
                connected_lock_clone,
            );
        });
    }
}

fn serial_thread(
    port_name: Arc<RwLock<String>>,
    terminator: String,
    settings: PortSettings,
    cmd_rx: Receiver<String>,
    resp_tx: Sender<Result<Packet>>,
    port_changed:Arc<RwLock<bool>>, 
    connected_lock:Arc<RwLock<bool>>, 
    firmware_data: Arc<Mutex<Vec<u8>>>,
    progress_tx: Sender<(String, u32, Option<String>)>  
) {
    loop: ...
}

Sorry I only skimmed your code because I couldn't immediatly see how the rust and the c++ code are related. But if you want multiple types implementing the same methods you use traits to define the behaviour + either generics or trait objects to use the types that implement that behavior.

struct SPortSettings;

pub trait Device {
    fn get_port_settings(&self, ps: &mut SPortSettings); // could also add a default impl
                                                         
    // More methods
}

struct SomeDevice {
    // Whatever this device needs
}

impl Device for SomeDevice {
    fn get_port_settings(&self, ps: &mut SPortSettings) {
        // actual implementation
    }

    // implement other methods
}

// Use in a function with generics
fn use_devices_generic<D: Device>(devices: &[D]) {
    // All devices have the same type
}

// Use in a function with trait object
fn use_devices_trait_object(devices: &[Box<dyn Device>]) {
    // devices can have different types
}

Playground

One big difference to c++ inheritance is that you cannot define fields on a trait that get set by the implementor (you can define constants on a trait though). One consequence is that default implementations for methods are usually constrained to whatever other non default implemented methods of the trait can provide.

3 Likes

Rather than having individual fields in Arc to share to the background thread, you'll get code structure closer to the C++ if you stick the entire Device into an Arc which gets shared.

Customization for each instrument would be via a trait instead of by inheritance.

I'll let others address other points here.

1 Like

"Prefer composition over inheritance" is decades old topic. You can find lots of articles and tutorials to write code in this way with this keyword. Since you're learning Rust now, it might be easier for you to refactor the C++ code to follow this pattern first. After that it might be a lot easier to port it to Rust code with similar behavior/structures.

6 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.