0

The CreateFile function is useful for opening files or devices for read/write access, providing a handle.

The third parameter, dwShareMode, specifies if the file/device can later be accessed by others. An example, with files:

void* pFileHandle1 = ::CreateFileA("C:\\test.txt", GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
DWORD lastError = GetLastError(); // 0, ERROR_SUCCESS
void* pFileHandle2 = ::CreateFileA("C:\\test.txt", GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
lastError = GetLastError(); // 0, ERROR_SUCCESS

All good here: we have 2 different handles that can read/write a single file.

But in my case, I want to use a COM port:

void* pComHandle1 = ::CreateFileA("\\\\.\\COM3", GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
lastError = GetLastError(); // 0, ERROR_SUCCESS
void* pComHandle2 = ::CreateFileA("\\\\.\\COM3", GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
lastError = GetLastError(); // 5, ERROR_ACCESS_DENIED Oops!

The first handle is valid and can be used, but the second one is INVALID_HANDLE_VALUE.

What's up with that? Can't you share COM ports that way?

Alex Millette
  • 239
  • 1
  • 10
  • 4
    A serial port such as "COM3" (or the native NT name such as "\Device\Serial2") is an exclusive Device object, i.e. when the device is defined and created by the driver function `serial!SerialCreateDevObj`, it calls [`IoCreateDevice`](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/content/wdm/nf-wdm-iocreatedevice) with `Exclusive` as true. The reason for exclusive access is that each application expects exclusive state and settings (e.g. bps, parity). That said, opening the Device returns a handle for a File object, which can be duplicated via `DuplicateHandle`. – Eryk Sun May 07 '18 at 16:38
  • @eryksun so you need to pass the handle (or duplicates) around? That seems difficult when 2 separate process need their own access... – Alex Millette May 07 '18 at 17:20
  • 1
    A child process can inherit the handle, or you can duplicate the handle into another process. This requires communicating the handle value in some way, such as in the command line string, an inherited environment variable, or a named pipe. But why are multiple processes reading from and writing to the same port? You'd be better off abstracting this behind another interface, such as a named pipe or socket, with a single server process that manages the serial port. – Eryk Sun May 07 '18 at 17:32
  • here even task not in that `\Device\SerialN` is exclusive (have flag `DO_EXCLUSIVE`) device. usually this is *FDO* device, attached to some *PDO*. and we can (and really must) open it by *PDO* interface name, returned by `CM_Get_Device_Interface_ListW(&GUID_DEVINTERFACE_COMPORT,..)`. this *PDO* not exclusive, but create handler for serial usually allow only single file object opened on device anyway. so you can have multiple handles, but only single file object for serial. – RbMm May 07 '18 at 17:55
  • "But why are multiple processes reading from and writing to the same port?" Hardware requirements. A single COM port could be used to communicate to 2 devices, with different pins. – Alex Millette May 07 '18 at 18:56
  • @AlexMillette: Well, serial ports don't support that usage. In the old 16550A UART, all the control pins are in one register, all the status pins in another. You cannot change just one pin, you have to read or write a whole register at once. If you had multiple processes touching different bits in the same register, you'd have a race condition. Modern serial ports don't have 16550s but the race condition problem still applies. You would need some arbiter that combines commands from different processes, and Windows doesn't provide one for serial ports. – Ben Voigt May 07 '18 at 19:03
  • So if you need to slice a serial port into logical units, make a server program that exclusively owns the serial port, and accepts socket connections from the different programs that need access. This server will have to combine the demands of the different programs into the actual serial port operations. – Ben Voigt May 07 '18 at 19:04
  • To restate RbMm, in this case the device has a well-known, friendly DOS name such as "COM3", which links to the FDO (e.g. "\Device\Serial2"). This is not considered a best practice, but this is a legacy setup. In general we open the PDO to access the FDO (device-stack top). This has an enumerated DOS name that links to a generically enumerated NT name. To discover the name, we query the device list for the interface class, and get device properties (e.g. port name). In this case the PDO isn't an exclusive device, but the serial device driver ensures this open path is exclusive as well. – Eryk Sun May 07 '18 at 20:17
  • usually `\??\COM_x` is symbolic link to `\Device\Serial_x` - so this is the same device. usual (in case hardware com port) this is *FDO* device attached to some hardware (base created) *PDO* which registered com interface on device. here question - from where we know that need use `COM0` (or `"\Device\Serial0`) name ? may be need `COM8` for example, correct way - get list of device interface for `GUID_DEVINTERFACE_COMPORT` and select name for open from this list. and name will be symlink to *PDO* in this case – RbMm May 07 '18 at 20:47
  • @RbMm, an application's settings could leave it completely up to the user to supply the name of an available port. As a middle ground, the application can use the device interface class to populate a selection box of device names showing the device "PortName" property. In this case, it would be best to also open the device using the automatically enumerated name from this list (e.g. something like "\\?\ACPI#PNP...{86e0d1e0-8089-11d0-9ce4-08003e301f73}"), so we're back to using the PDO to access the FDO. Good, since not naming FDOs is considered a better practice anyway. – Eryk Sun May 07 '18 at 21:18

1 Answers1

4

Quoting the documentation for CreateFile:

The CreateFile function can create a handle to a communications resource, such as the serial port COM1. For communications resources, the dwCreationDisposition parameter must be OPEN_EXISTING, the dwShareMode parameter must be zero (exclusive access), and the hTemplateFile parameter must be NULL. Read, write, or read/write access can be specified, and the handle can be opened for overlapped I/O.

The implication from the documentation here is that communication objects cannot be shared like ordinary files. The Windows API leaves it to whoever opened the port to decide how/if they want to share access to that resource, and leaves them to manage the consequences of that decision.

To share the port, you can use DuplicateHandle and pass that to whoever you want to grant access to the port after you've opened it. For further reading, check out this ancient article from MSDN

That said, if you want to share a COM port across multiple processes, you're better off opening it in only one of them, and using some form of IPC to transfer data. Let one process handle servicing the port.

theB
  • 6,450
  • 1
  • 28
  • 38
  • 2
    When opening a serial port, `dwShareMode` is irrelevant in the NT implementation of the Windows API. (I can't say how the long-dead Windows 9x implementation handled this.) Device exclusivity is on another level than I/O read-write-delete sharing. An exclusive device can't be opened a second time for any access at all. Using 0 for `dwShareMode` in this case is a form of self-documenting consistency in one's code, but doesn't actually change anything. – Eryk Sun May 07 '18 at 17:46
  • @eryksun - task even not in that *FDO* serial device is exclusive. the *PDO* to which it attached - not exclusive. and we can open really by *PDO* name. but `irp_mj_create` handler for standard serial anyway return access denied if already open some file object on device – RbMm May 07 '18 at 18:02
  • Can confirm what eryksun said: dwShareMode can be set to something else than 0, but I suppose it is disregarded. – Alex Millette May 07 '18 at 18:54
  • @AlexMillette - the `dwShareMode` is interpret only by specific device to which you send request. usually only file system device use it. most devices (including serial) ignore it. – RbMm May 07 '18 at 19:43
  • 1
    @RbMm, generally the share mode is used with a system-defined `SHARE_ACCESS` structure and I/O manager functions such as `IoCheckShareAccess`. But the named-pipe file system has an interesting application of the share mode for a pipe instance. It's not exposed in the Windows API, but if we call NTAPI `NtCreateNamedPipeFile`, then an inbound pipe is created with write sharing, an outbound pipe is created with read sharing, and a duplex pipe is created with read-write sharing -- so the client end can be opened with write, read, and read-write access, respectively. – Eryk Sun May 07 '18 at 20:30
  • @eryksun - `IoCheckShareAccess` not called by IO manager when user ask open file. it called by device only, if device care about share access. if device not care - nobody called this api. so it only api helper for devices (primary FS devices) which use share access – RbMm May 07 '18 at 20:40
  • @RbMm, I know that. I was clarifying that file systems generally do not freely interpret the share-mode flags. They use a well-known system structure and functions to provide consistent behavior. But then, as a counter-example, there's the named-pipe "file system", which does its own thing here. – Eryk Sun May 07 '18 at 20:45
  • @eryksun - yes, but i mean that if device not care about share access - nobody care. how it check this - already another question. com port at al not care (if this is serial.sys).about named-pipe is interesting. dont know about this before – RbMm May 07 '18 at 20:47