Mac设备访问在C中运行正常,但在Java/JNA中的等效代码却不工作。

huangapple go评论106阅读模式
英文:

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

问题

以下是翻译好的内容:

public class Cli
{
    /**
     * Java中对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.
英文:

We work with a serial device that is attached to a Mac on USB and need to configure the DTR/RTS-line settings. Technically this involves usage of open(3), ioctl(3).

We implemented this in C and it worked. Below is a very simplified snippet showing the core part.

Then we migrated the code to Java/JNA and run into the problem that the ported code did not work, though it is basically a line-by-line conversion of the C code.

The question is where do we go wrong?

Symptom of the failure in Java is errno=25 'Inappropriate ioctl for device' returned from the call to ioctl(). Since it works in C it seems that we do somwthing wrong in JNA.

What we did:

  • Checked the values from the header constants. Note that the C-code generates Java-compatible constant definitions that are used in the Java code.
  • Checked the signature of ioctl(). Seems to be right according to the man-page and include files.
  • Guessed that the problem is that the ioctl code for TIOCMSET is not properly passed since it is negative.

We use JNA 5.5.0.

Here comes the C code. The snippet simply reads the settings of the lines and writes them back unmodified for demo purposes. Here's the code (note the hard-coded device name).

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;
}

Output of the above is:

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

Here's the Java implementation:

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 output is:

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

答案1

得分: 2

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);

“获取”似乎可以工作,因为本机代码只填充了8字节中的4字节,并且恰好是低位字节,但您可能会因为超额分配而破坏堆栈。

正如@nyholku在评论中指出的,除了从long切换到int之外,您可能还需要将int(而不是指针)传递给命令的TIOCMSET版本。有相互冲突的文档,但我在实际示例中看到的示例更偏向于您的指针实现。

因此,您的代码应包括:

IntByReference lineStatus = new IntByReference();
int rc = C.ioctl(portfd, TIOCMGET, lineStatus);
// 根据@nyholku链接的页面,可能如下:
rc = C.ioctl(portfd, TIOCMSET, lineStatus.getValue());
// 根据手册页和其他示例,很可能是这样的:
rc = C.ioctl(portfd, TIOCMSET, lineStatus);

您确实说C版本的非指针版本“有效”,但仅在不引发错误的情况下。为了确认什么“有效”,您应该再次读取字节,以确保您设置的内容实际上生效。

英文:

A review of the ioctl.h header file for the commands you are using reveals that it expects an int as the third argument:

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

You correctly define a 4-byte int in your C code and pass a reference to it, which works:

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

However, in your Java code, you define an 8-byte long reference to pass:

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

The "get" appears to work because the native code is only filling in 4 of the 8 bytes and it happens to be the low order bytes, but you may be corrupting the stack with the overallocation.

As @nyholku points out in the comments, in addition from switching from long to int you may need to pass the int (rather than the pointer) to the TIOCMSET version of the command. There is conflicting documentation but the examples I see in the wild favor your pointer implementation.

So your code should include:

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 );

You do say the C version without the pointer "works" but only in the sense that it does not throw an error. To confirm what "works", you should read the bytes again to make sure whatever you set actually stuck.

答案2

得分: 1

我们多次检查了第三个参数。指针必须被传递还是不必传递?我们找到的文档——在 Mac 上的 Man 手册 man -s 4 tty——确实记录了要传递一个指针。因此,Unix 实现之间似乎存在差异。

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

问题出在这行代码上

        long TIOCMSET = 0x8004746d;

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

        long TIOCMSET = 0x8004746dL; // 注意末尾的 'L'。

一切都正常工作了。在其他的 Unix 系统中,我们没有这个问题,因为 TIO... 常量恰好是正数。

英文:

We checked many times the third argument. Must a pointer be passed or not? The docs we found -- Man page man -s 4 tty on the Mac -- really documents to pass a pointer. So there seem to be differences between Unix implementations.

Finally we found the solution by printing the passed value using printf( "%xl", ... );. and the resulting value was 0xffffffff8004746d. So we got an unexpected sign extension.

And the problem is the line

        long TIOCMSET = 0x8004746d;

The literal constant is defined as an int literal that gets implicitly converted to long with sign extension. Since 0xffffffff8004746d is not equal to 0x8004746d' this explains the error message inappropriate ioctl for device. When we changed the line above to

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

everything worked perfectly. On other Unixes we did not have the problem because the TIO... constants happened to be positive.

huangapple
  • 本文由 发表于 2020年4月9日 18:11:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/61118784.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定