bottom part of serial port support on Unix #29033
Conversation
tagging this 2.2.0 per #29033 |
internal static partial class Sys | ||
{ | ||
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_TermiosReset", SetLastError = true)] | ||
internal static extern int TerminalReset(SafeFileHandle fd, int speed, int data, StopBits stop, Parity parity, Handshake flow); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These methods should have the same name as the native counterpart, minus the "SystemNative_" prefix, so e.g. TermiosReset instead of TerminalReset.
@@ -0,0 +1,360 @@ | |||
// Licensed to the .NET Foundation under one or more agreements. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extern "C" int32_t SystemNative_TermiosGetDtr(int fd) | ||
{ | ||
return SystemNative_TermiosGetSignal(fd, TIOCM_DTR); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to expose so many externs? Or could we just expose TermiosGetSignal as well as a PAL'd set of symbols for the bit argument?
case B75: | ||
return 75; | ||
case B50: | ||
return 50; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do these constant B values not always have the same numerical value as what their name suggests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They don't, they have completely different values. They are enum-like, with values going up by one upto B38400. Above that, there is a large jump in the numbering and then they go again by one.
/** | ||
* Get states of serial line. | ||
*/ | ||
extern "C" int32_t SystemNative_TermiosGetDcd(int fd); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use BEGIN_EXTERN_C
and END_EXTERN_C
before and after the functions declarations, that will avoid you to repeat extern "C"
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or we can simply rename pal_termios.cpp
to pal_termios.c
, as there is no real dependency on libstdc++ in there, and we already have some C files in the codebase.
I know it was discussed previously, but I really see no harm either way. It's just a matter of picking dependencies sparingly for each build unit separately (just like I wouldn't add reference to a package in managed code unless I need it), otherwise one can always argue "we already depend on libstdc++ for 10 other reasons".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You still need to be able to be included in C++ files, thus the need for extern "C"
. My comment is only to help you reduce repetition, it's not a mandated or required change.
internal static extern int TerminalReset(SafeFileHandle fd, int speed, int data, StopBits stop, Parity parity, Handshake flow); | ||
|
||
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_TermiosGetDcd", SetLastError = true)] | ||
internal static extern int TerminalGetCd(SafeFileHandle fd); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are missing "D" in the function name - the function should end "GetDcd" instead of "GetCd".
} | ||
|
||
static speed_t | ||
SystemNative_TermiosSpeed2rate(int speed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that the "r" in the "rate" in the function name should be upper case
} | ||
|
||
static int | ||
SystemNative_TermiosRate2speed(speed_t brate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that the "s" in the "speed" in the function name should be upper case
case B75: | ||
return 75; | ||
case B50: | ||
return 50; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They don't, they have completely different values. They are enum-like, with values going up by one upto B38400. Above that, there is a large jump in the numbering and then they go again by one.
term.c_cflag &= ~(static_cast<tcflag_t>(PARODD)); | ||
break; | ||
default: | ||
term.c_cflag &= ~(static_cast<tcflag_t>(PARENB | PARODD)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit - here you clear the parity bits in the default and in the code below, you pre-clear the xon/xoff and rts/cts bits. It would be nice to be consistent.
Thanks for all suggestions. I’m out without computer access until 5/8. I’ll deal with feedback when I get back. |
Copying comment from #28415 (comment) here as well: System.IO.Ports is standalone package, but this is adding dependency on System.Native. System.Native is internal implementation detail. We do not want things outside .NET Core distro to depend on it. Should System.IO.Ports have its own supporting native library instead? Or depend on Mono.Posix? |
case 50: | ||
return B50; | ||
} | ||
return ((speed_t)(speed)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should return some indication of failure when we don't match any of the supported speeds instead of casting just the passed in value to the speed_t
in case we don't find any match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Linux supports custom speeds but I did not get to testing it.
I decided to return 0 if speed does not match one of the supported values and treat it as error.
On Unix B0 has special meaning but I don't think it is on Windows. So I think it is ok to use it. speed_t is unsigned long so we can possibly use other value.
static int TermiosGetStatus(int32_t fd) | ||
{ | ||
int status = 0; | ||
if(ioctl(fd, TIOCMGET, &status) < 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit - please add space between the if
and the opening (
at multiple places.
return -1; | ||
} | ||
|
||
status = TermiosGetStatus(fd); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit - please remove one extra space between the status
and =
{ | ||
switch (speed) | ||
{ | ||
case 230400: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can see that there are more speeds (upto B4000000) on my Ubuntu 16.04. Should we support all of them here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The high speeds are not portable. OSX does not have them and FreeBSD has some.
So I added them conditionally and we should be able to support them on platforms where available.
int32_t | ||
TermiosGetSpeed(int32_t fd) | ||
{ | ||
struct termios term; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit - can you please remove the extra spaces between termios
and term
here and in the TermiosSetSpeed
and TermiosReset
functions below?
TermiosAvailableBytes(int32_t fd, int32_t readBuffer) | ||
{ | ||
int32_t bytes; | ||
if (ioctl (fd, readBuffer ? FIONREAD : TIOCOUTQ, &bytes) == -1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit - please put the {
on a separate line.
int32_t | ||
TermiosDiscard(int32_t fd, int32_t queue) | ||
{ | ||
return tcflush(fd, queue == 0 ? TCIOFLUSH : queue == 1 ? TCIFLUSH : TCOFLUSH); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit - would you mind adding parentheses around each of the conditions here to make it easier to read?
|
||
cfmakeraw(&term); | ||
term.c_cflag |= (CLOCAL | CREAD); | ||
term.c_lflag &= ~((tcflag_t)(ICANON | ECHO | ECHOE | ECHOK | ECHONL | ISIG | IEXTEN )); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder - why don't we set all the flags ourselves and depend on the preinitialization by the cfmakeraw instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we possibly could. In some ways I like it explicit as I see what this exactly does without looking at man pages (with cfmakeraw as base line)
Even if not supported in first round, I assume serial port will be supported on all Unix like systems so I would need to also verify that cfmakeraw() works same on all OSes.
AllQueues = 3, | ||
}; | ||
|
||
[DllImport(Libraries.IOPortsNative, EntryPoint = "TermiosReset", SetLastError = true)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at other mini-PALs we have, it looks like the native functions should be named with underscore between the Termios
and the rest of the function name to match the convention used elsewhere. So e.g. Termios_Reset
.
internal static extern int TermiosGetSignal(SafeFileHandle fd, Signals signal, int set); | ||
|
||
[DllImport(Libraries.IOPortsNative, EntryPoint = "Termios_SetSpeed", SetLastError = true)] | ||
internal static extern int TermiosSetSpeed(SafeFileHandle fd, int speed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The speed
argument of the unmanaged method is unsigned.
|
||
int32_t Termios_AvailableBytes(int32_t fd, int32_t readBuffer); | ||
|
||
int32_t Termios_Reset(int32_t fd, int speed, int dataBits, int stopBits, int parity, int handshake); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We prefer explicitly sized types. From https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/interop-guidelines.md#unix-shims:
Use int32_t, int64_t, etc. from stdint.h and not int, long, etc.
Windows Nano test failures are unrelated to this. |
}; | ||
|
||
/* Interop/Unix/Interop.Termios.cs */ | ||
enum { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit - can you please put the curly brace on a separate line like in the previous enums?
SignalDcd = 5, | ||
}; | ||
|
||
enum { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit - can you please put the curly brace on a separate line like in the previous enums?
{ | ||
internal enum Signals | ||
{ | ||
SignalDtr = 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub, what is the convention for enums shared between native and managed code - should the names of the members be prefixed by the "Signal" even on the managed side?
And, should the name of the enum type really be plural?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should the names of the members be prefixed by the "Signal" even on the managed side?
In most cases we name the enum value the same as the native symbol and we name the enum something meaningful / descriptive, e.g.
[Flags]
internal enum MemoryMappedProtections
{
PROT_NONE = 0x0,
PROT_READ = 0x1,
PROT_WRITE = 0x2,
PROT_EXEC = 0x4
}
{ | ||
internal static partial class Serial | ||
{ | ||
[DllImport(Libraries.IOPortsNative, EntryPoint = "SerialPortOpen", SetLastError = true)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that even for this function, we should follow the convention of other similar ones and name it with Serial_
prefix. Maybe Serial_OpenPort
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we should prefix all of the exports with a name related to the library, e.g. everything in System.Native.so is prefixed with SystemNative_
.
|
||
BEGIN_EXTERN_C | ||
|
||
int SerialPortOpen(const char * name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've generally prefixed all exports with the name of the library, e.g. SystemIoPortsNative_SerialPortOpen.
SendQueue = 2, | ||
}; | ||
|
||
static int TermiosGetStatus(int32_t fd) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other libraries, we've used intptr_t
for file descriptors at the interop layer. Then we can use SafeHandle
in the P/Invokes. We should do the same here, e.g.
corefx/src/Native/Unix/System.Native/pal_networking.c
Lines 986 to 993 in 95b2163
int32_t SystemNative_GetIPv6MulticastOption(intptr_t socket, int32_t multicastOption, IPv6MulticastOption* option) | |
{ | |
if (option == NULL) | |
{ | |
return Error_EFAULT; | |
} | |
int fd = ToFileDescriptor(socket); |
|
||
int32_t Termios_SetSignal(int32_t fd, int32_t signal, int32_t set) | ||
{ | ||
int status; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be moved down to where it's actually initialized?
} | ||
|
||
static speed_t | ||
TermiosSpeed2Rate(int speed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: why is this on its own line?
} | ||
|
||
int32_t | ||
Termios_GetSpeed(int32_t fd) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question for all of these... why have a newline between the return type and the name?
|
||
|
||
/* Open device file in non-blocking mode and without controlling terminal */ | ||
int SerialPortOpen(const char * name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As noted in another comment, we should represent file descriptors at the interop layer as intptr_t. As it's currently written, on a 64-bit platform there's potentially going to be corruption here, as this is going to return a 32-bit value and the managed code is expected a 64-bit value.
@@ -0,0 +1,22 @@ | |||
project(System.IO.Ports.Native) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add C
project(System.IO.Ports.Native C)
#include "pal_types.h" | ||
#include "pal_compiler.h" | ||
|
||
BEGIN_EXTERN_C |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This macro and its END_ counterpart are not defined anymore and can be removed. Now we are pure C.
|
||
BEGIN_EXTERN_C | ||
|
||
intptr_t SystemIoPortsNative_SerialPortOpen(const char * name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefix misssing:
DLLEXPORT intptr_t
same for add SystemIOPortNative_*
in .h
files only.
return status; | ||
} | ||
|
||
int32_t SystemIoPortsNative_TermiosGetSignal(intptr_t handle, int32_t signal) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Casing of IO, should be: SystemIOPortsNative_
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know. I used prefix @stephentoub suggested.
while ((result = open(name, O_RDWR | O_NOCTTY | O_CLOEXEC | O_NONBLOCK)) < 0 && errno == EINTR); | ||
return result; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: there are two extra blank lines at the end of this file, reduce it to one blank line?
#include <errno.h> | ||
#include <pal_serial.h> | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we have one blank line elsewhere
return -1; | ||
} | ||
|
||
static speed_t SystemIoPortsNative_TermiosSpeed2Rate(int speed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: you don't need the prefix for non-exported functions, though it's fine to have it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I previously did not have them for internal static functions.
But I decided it may be better to just keep everything same.
Thanks for all your feedback.
@wfurt, looks like this needs to be rebased. |
Actually, I'm wrong, it's fine for a squash and merge. It would only have issues with a rebase and merge because some of the earlier commits have conflicts. |
* bottom part of serial port support * small cleanup * feedback from reviews * remove previous changes to System.Native * feedback from review * use consistently int32_t * update names and types based on feedback * feedback from kasper3 Commit migrated from dotnet/corefx@83753bc
This is part of original #28415 after addressing feedback. We publish System.IO.Ports as separate package so we can iterate on upper parts later.
I'm at the point where I can run Serial port tests and I still get 20-30 failures (out of few hundred) depending in what HW I uses. The upper part still may need some polishing so as the test suite.
In general issues seems to be in two areas: