Mac 设备访问在 C 中有效,但在 Java/JNA 中的等效代码无效

Mac device access works in C, but equivalent code in Java/JNA not

我们使用连接到 USB 上 Mac 的串行设备,需要配置 DTR/RTS-line 设置。从技术上讲,这涉及 open(3)ioctl(3).

的用法

我们用 C 语言实现了它并且它起作用了。下面是显示核心部分的非常简化的片段。

然后我们将代码迁移到 Java/JNA 和 运行 到移植代码 工作的问题,尽管它基本上是 line-by-line C代码的转换。

问题是我们哪里出错了?

Java 中失败的症状是 errno=25 'Inappropriate ioctl for device' 从对 ioctl() 的调用返回。因为它在 C 中工作,所以我们在 JNA 中似乎做错了一些事情。

我们做了什么:

我们使用 JNA 5.5.0。

C代码来了。代码片段只是读取线路的设置并将它们原封不动地写回以用于演示目的。这是代码(注意 hard-coded 设备名称)。

int main(int argc, char **argv)
{
    // Print constant values.
    printf( "long TIOCMGET = 0x%x;\n", TIOCMGET );
    printf( "long TIOCMSET = 0x%x;\n", TIOCMSET );
    printf( "int O_RDWR = 0x%x;\n", O_RDWR );
    printf( "int O_NDELAY = 0x%x;\n", O_NDELAY );
    printf( "int O_NOCTTY = 0x%x;\n", O_NOCTTY );

    int value = O_RDWR|O_NDELAY|O_NOCTTY;
    printf( "value=%x\n", value );
    int portfd = open("/dev/tty.usbmodem735ae091", value);
    printf( "portfd=%d\n", portfd );

    int lineStatus;
    printf( "TIOCMGET %x\n", TIOCMGET );
    int rc = ioctl( portfd, TIOCMGET, &lineStatus );
    printf( "rc=%d, linestatus=%x\n", rc, lineStatus );

    rc = ioctl( portfd, TIOCMSET, &lineStatus );
    printf( "rc=%d, linestatus=%x\n", rc, lineStatus );

    if ( rc == -1 )
        printf( "Failure\n" );
    else
        printf( "Success\n" );

    if ( portfd != -1 )
        close( portfd );

    return 0;
}

上面的输出是:

long TIOCMGET = 0x4004746a;
long TIOCMSET = 0x8004746d;
int O_RDWR = 0x2;
int O_NDELAY = 0x4;
int O_NOCTTY = 0x20000;
value=20006
portfd=3
TIOCMGET 4004746a
rc=0, linestatus=6
rc=0, linestatus=6
Success

这是 Java 实施:

public class Cli
{
    /**
     * Java mapping for lib c
     */
    public interface MacCl extends Library {
        String NAME = "c";
        MacCl INSTANCE = Native.load(NAME, MacCl.class);

        int open(String pathname, int flags);
        int close(int fd);
        int ioctl(int fd, long param, LongByReference request);
        String strerror( int errno );
    }

    private static final MacCl C = MacCl.INSTANCE;

    private static PrintStream out = System.err;

    public static void main( String[] argv )
    {
        long TIOCMGET = 0x4004746a;
        long TIOCMSET = 0x8004746d;
        int O_RDWR = 0x2;
        int O_NDELAY = 0x4;
        int O_NOCTTY = 0x20000;

        int value = O_RDWR|O_NDELAY|O_NOCTTY;
        out.printf( "value=%x\n", value );
        int portfd = C.open(
                "/dev/tty.usbmodem735ae091",
                value );
        out.printf( "portfd=%d\n", portfd );

        LongByReference lineStatus = new LongByReference();

        int rc = C.ioctl( portfd, TIOCMGET, lineStatus );
        out.printf(
                "rc=%d, linestatus=%d\n", rc, lineStatus.getValue() );

        rc = C.ioctl( portfd, TIOCMSET, lineStatus );
        out.printf(
                "rc=%d errno='%s'\n",
                rc,
                C.strerror( Native.getLastError() ) );

        if ( rc == -1 )
            out.print( "Failure." );
        else
            out.print( "Success." );

        if ( portfd != -1 )
            C.close( portfd );
    }
}

Java 输出为:

value=20006
portfd=23
rc=0, linestatus=6
rc=-1 errno='Inappropriate ioctl for device'
Failure.

查看您正在使用的命令的 ioctl.h 头文件表明它需要一个 int 作为第三个参数:

#define TIOCMSET    _IOW('t', 109, int) /* set all modem bits */
#define TIOCMGET    _IOR('t', 106, int) /* get all modem bits */

您在 C 代码中正确定义了一个 4 字节 int 并传递了对它的引用,这有效:

int lineStatus;
int rc = ioctl( portfd, TIOCMGET, &lineStatus );
rc = ioctl( portfd, TIOCMSET, &lineStatus );

但是,在您的 Java 代码中,您定义了一个 8 字节的 long 引用来传递:

LongByReference lineStatus = new LongByReference();
int rc = C.ioctl( portfd, TIOCMGET, lineStatus );
rc = C.ioctl( portfd, TIOCMSET, lineStatus );

"get" 似乎可以工作,因为本机代码仅填充 8 个字节中的 4 个,而且恰好是低位字节,但您可能会因过度分配而损坏堆栈。

正如@nyholku 在评论中指出的那样,除了从 long 切换到 int 之外,您可能还需要将 int (而不是指针)传递给 TIOCMSET 命令的版本。文档存在冲突,但我在野外看到的示例支持您的指针实现。

因此您的代码应包括:

IntByReference lineStatus = new IntByReference();
int rc = C.ioctl( portfd, TIOCMGET, lineStatus );
// Possible, per the page @nyholku linked:
rc = C.ioctl( portfd, TIOCMSET, lineStatus.getValue() );
// Probable, per the man pages and other examples:
rc = C.ioctl( portfd, TIOCMSET, lineStatus );

你说的是没有指针的 C 版本 "works" 但只是在它不会抛出错误的意义上。要确认什么 "works",您应该再次读取字节以确保您设置的内容确实卡住了。

我们检查了很多次第三个参数。是否必须传递指针?我们找到的文档——Mac 上的手册页 man -s 4 tty——确实是传递指针的文档。所以 Unix 实现之间似乎存在差异。

最后我们通过使用 printf( "%xl", ... ); 打印传递的值找到了解决方案。结果值为 0xffffffff8004746d。所以我们得到了意想不到的符号扩展。

问题是行

        long TIOCMSET = 0x8004746d;

文字常量定义为 int 文字,它隐式转换为带符号扩展 的长 。由于 0xffffffff8004746d 不等于 0x8004746d' 这解释了错误消息 inappropriate ioctl for device。当我们将上面的行更改为

        long TIOCMSET = 0x8004746dL; // Note the 'L' at the end.

一切都很完美。在其他 Unix 上我们没有问题,因为 TIO... 常量恰好是正数。