Implementing a virtual serial port using DriverKit/SerialDriverKit

I'm trying to implement a virtual serial port driver for my ham radio projects which require emulating some serial port devices and I need to have a "backend" to translate the commands received by the virtual serial port into some network-based communications. I think the best way to do that is to subclass IOUserSerial? Based on the available docs on this class (https://developer.apple.com/documentation/serialdriverkit/iouserserial), I've done the basic implementation below. When the driver gets loaded, I can see sth like tty.serial-1000008DD in /dev and I can use picocom to do I/O on the virtual serial port. And I see TxDataAvailable() gets called every time I type a character in picocom.

The problems are however, firstly, when TxDataAvailable() is called, the TX buffer is all-zero so although the driver knows there is some incoming data received from picocom, it cannot actually see the data in neither Tx/Rx buffers.

Secondly, I couldn't figure out how to notify the system that there are data available for sending back to picocom. I call RxDataAvailable(), but nothing appears on picocom, and RxFreeSpaceAvailable() never gets called back. So I think I must be doing something wrong somewhere. Really appreciate it if anyone could point out how should I fix it, many thanks!

VirtualSerialPortDriver.cpp:

constexpr int bufferSize = 2048;

using SerialPortInterface = driverkit::serial::SerialPortInterface;

struct VirtualSerialPortDriver_IVars
{
    IOBufferMemoryDescriptor *ifmd, *rxq, *txq;
    SerialPortInterface *interface;
    uint64_t rx_buf, tx_buf;
    bool dtr, rts;
};

bool VirtualSerialPortDriver::init()
{
    bool result = false;
    result = super::init();
    if (result != true)
    {
        goto Exit;
    }
    ivars = IONewZero(VirtualSerialPortDriver_IVars, 1);
    if (ivars == nullptr)
    {
        goto Exit;
    }
    kern_return_t ret;
    ret = ivars->rxq->Create(kIOMemoryDirectionInOut, bufferSize, 0, &ivars->rxq);
    if (ret != kIOReturnSuccess) {
        goto Exit;
    }
    ret = ivars->txq->Create(kIOMemoryDirectionInOut, bufferSize, 0, &ivars->txq);
    if (ret != kIOReturnSuccess) {
        goto Exit;
    }
    IOAddressSegment ioaddrseg;
    ivars->rxq->GetAddressRange(&ioaddrseg);
    ivars->rx_buf = ioaddrseg.address;
    ivars->txq->GetAddressRange(&ioaddrseg);
    ivars->tx_buf = ioaddrseg.address;
    return true;
Exit:
    return false;
}

kern_return_t
IMPL(VirtualSerialPortDriver, HwActivate)
{
    kern_return_t ret;
    ret = HwActivate(SUPERDISPATCH);
    if (ret != kIOReturnSuccess) {
        goto Exit;
    }
    // Loopback, set CTS to RTS, set DSR and DCD to DTR
    ret = SetModemStatus(ivars->rts, ivars->dtr, false, ivars->dtr);
    if (ret != kIOReturnSuccess) {
        goto Exit;
    }
Exit:
    return ret;
}

kern_return_t
IMPL(VirtualSerialPortDriver, HwDeactivate)
{
    kern_return_t ret;
    ret = HwDeactivate(SUPERDISPATCH);
    if (ret != kIOReturnSuccess) {
        goto Exit;
    }
Exit:
    return ret;
}

kern_return_t
IMPL(VirtualSerialPortDriver, Start)
{
    kern_return_t ret;
    ret = Start(provider, SUPERDISPATCH);
    if (ret != kIOReturnSuccess) {
        return ret;
    }
    IOMemoryDescriptor *rxq_, *txq_;
    ret = ConnectQueues(&ivars->ifmd, &rxq_, &txq_, ivars->rxq, ivars->txq, 0, 0, 11, 11);
    if (ret != kIOReturnSuccess) {
        return ret;
    }
    IOAddressSegment ioaddrseg;
    ivars->ifmd->GetAddressRange(&ioaddrseg);
    ivars->interface = reinterpret_cast<SerialPortInterface*>(ioaddrseg.address);
    SerialPortInterface &intf = *ivars->interface;
    ret = RegisterService();

    if (ret != kIOReturnSuccess) {
        goto Exit;
    }
    TxFreeSpaceAvailable();
Exit:
    return ret;
}

void
IMPL(VirtualSerialPortDriver, TxDataAvailable)
{
    SerialPortInterface &intf = *ivars->interface;
    // Loopback
    // FIXME consider wrapped case
    size_t tx_buf_sz = intf.txPI - intf.txCI;
    void *src = reinterpret_cast<void *>(ivars->tx_buf + intf.txCI);
//    char src[] = "Hello, World!";
    void *dest = reinterpret_cast<void *>(ivars->rx_buf + intf.rxPI);
    memcpy(dest, src, tx_buf_sz);
    intf.rxPI += tx_buf_sz;

    RxDataAvailable();
    intf.txCI = intf.txPI;
    TxFreeSpaceAvailable();
    Log("[TX Buf]: %{public}s", reinterpret_cast<char *>(ivars->tx_buf));
    Log("[RX Buf]: %{public}s", reinterpret_cast<char *>(ivars->rx_buf));
    // dmesg confirms both buffers are all-zero
    Log("[TX] txPI: %d, txCI: %d, rxPI: %d, rxCI: %d, txqoffset: %d, rxqoffset: %d, txlogsz: %d, rxlogsz: %d",
        intf.txPI, intf.txCI, intf.rxPI, intf.rxCI, intf.txqoffset, intf.rxqoffset, intf.txqlogsz, intf.rxqlogsz);
}

void
IMPL(VirtualSerialPortDriver, RxFreeSpaceAvailable)
{
    Log("RxFreeSpaceAvailable() called!");
}

kern_return_t   IMPL(VirtualSerialPortDriver,HwResetFIFO){
    Log("HwResetFIFO() called with tx: %d, rx: %d!", tx, rx);
    kern_return_t ret = kIOReturnSuccess;
    return ret;
}

kern_return_t   IMPL(VirtualSerialPortDriver,HwSendBreak){
    Log("HwSendBreak() called!");
    kern_return_t ret = kIOReturnSuccess;
    return ret;
}

kern_return_t   IMPL(VirtualSerialPortDriver,HwProgramUART){
    Log("HwProgramUART() called, BaudRate: %u, nD: %d, nS: %d, P: %d!", baudRate, nDataBits, nHalfStopBits, parity);
    kern_return_t ret = kIOReturnSuccess;
    return ret;
}

    
kern_return_t   IMPL(VirtualSerialPortDriver,HwProgramBaudRate){
    Log("HwProgramBaudRate() called, BaudRate = %d!", baudRate);
    kern_return_t ret = kIOReturnSuccess;
    return ret;
}

kern_return_t   IMPL(VirtualSerialPortDriver,HwProgramMCR){
    Log("HwProgramMCR() called, DTR: %d, RTS: %d!", dtr, rts);
    ivars->dtr = dtr;
    ivars->rts = rts;
    kern_return_t ret = kIOReturnSuccess;
Exit:
    return ret;
}

kern_return_t  IMPL(VirtualSerialPortDriver, HwGetModemStatus){
    *cts = ivars->rts;
    *dsr = ivars->dtr;
    *ri = false;
    *dcd = ivars->dtr;
    Log("HwGetModemStatus() called, returning CTS=%d, DSR=%d, RI=%d, DCD=%d!", *cts, *dsr, *ri, *dcd);
    kern_return_t ret = kIOReturnSuccess;
    return ret;
}

kern_return_t   IMPL(VirtualSerialPortDriver,HwProgramLatencyTimer){
    Log("HwProgramLatencyTimer() called!");
    kern_return_t ret = kIOReturnSuccess;
    return ret;
}

kern_return_t   IMPL(VirtualSerialPortDriver,HwProgramFlowControl){
    Log("HwProgramFlowControl() called! arg: %u, xon: %d, xoff: %d", arg, xon, xoff);
    kern_return_t ret = kIOReturnSuccess;
Exit:
    return ret;
}
Implementing a virtual serial port using DriverKit/SerialDriverKit
 
 
Q