在Java中复制C结构体填充

5
根据这里的内容,当将结构写入二进制文件时,C编译器会填充值。正如链接中的示例所示,当写入以下结构时:
struct {
 char c;
 int i;
} a;

当编译器将数据写入二进制文件时,通常会在 char 和 int 字段之间留下一个未命名的、未使用的空白区域,以确保 int 字段被正确对齐。

我该如何使用另一种语言(比如 Java)创建与 C 生成的二进制输出文件完全相同的副本呢?

是否有自动方式可以在 Java 输出中应用 C 填充?还是我需要查看编译器文档来了解其工作原理(这里使用的编译器是 g++)?


值得注意的是,在Java中,char是一个16位的值,而不像在C中是8位。我建议您使用类似ByteBuffer.putInt(0, c & 0xFF)的方法。顺便问一下,您知道数据是大端还是小端吗?在C中,这取决于您所运行的机器的体系结构。 - Peter Lawrey
11个回答

14

不要这样做,它很脆弱,会导致对齐和字节顺序的问题。

对于外部数据而言,更好的方法是明确定义以字节为单位的格式,并编写显式函数来在内部和外部格式之间进行转换,使用移位和掩码(而不是 union!)。


XML 在这里也许有一席之地? - Preet Sangha
1
@starblue XML似乎是解决所有问题的万能钥匙! - Bombe
@starblue 谢谢回答。编写显式文件转换并不是很可能,因为有许多需要使用Java输出的C结构体(这些结构体的格式可能会不时地更改)。我感觉我可能得实现与g++在Java中输出时使用的相同形式的填充(我同意你这是容易出问题的)。唯一的救赎是平台和编译器不会改变,因此字节大小、字节序等也不应该改变。 - Lehane
你也可以尝试通过添加__attribute__((packed))来使结构紧凑。另一种解决方案是编写适当的I/O函数的代码生成器。 - starblue
@Bombe 你是否考虑过在xml中使用10兆像素的HDR图像哈哈。一个好的文件格式应该考虑到与目标处理器大多数的字节序相同的问题,这对于填充规则也是如此。在Windows位图中,扫描线总是从4个字节边界开始。整个二进制兼容的方式将加快速度。 - user877329

8
这不仅适用于写入文件,也适用于在内存中。由于结构体在内存中是有填充的,如果按字节顺序写出结构体,则填充会出现在文件中。
通常很难确定确切的填充方案,尽管我猜测一些启发式方法可以让您走得更远。如果您有结构体声明进行分析,则会有所帮助。
通常,大于一个字符的字段将对齐,以使其在结构体内部的起始偏移量成为其大小的倍数。这意味着short通常位于偶数偏移量上(假设sizeof(short)==2),而double等则位于可被8整除的偏移量上。 更新:正因为这样的原因(以及与字节顺序有关的原因),通常不建议将整个结构体转储到文件中。最好是按字段进行操作,如下所示:
put_char(out, a.c);
put_int(out, a.i);

假设put函数只写入值所需的字节,则这将向文件发出无填充版本的结构体,从而解决了问题。通过适当编写这些函数,还可以确保正确、已知的字节顺序。

谢谢,不幸的是我没有办法改变C结构体输出到文件的方式。我能够通过使用你提到的简单填充方法在Java中获得相同的输出。你知道C编译器实现填充的方式是否会有很大的差异吗? - Lehane
这主要取决于处理器架构,因此一旦您将其移植到其他架构上(我们最近就遇到了这种情况),真正的“乐趣”就开始了。这并不意味着在同一架构上的编译器之间保证相同。 - starblue
这篇文章谈到了结构体打包的一些内容,以及为什么会应用填充。链接:http://www.catb.org/esr/structure-packing/ - Michael Böckling

5
有没有一种自动的方法来在Java输出中应用C填充?还是我必须查看编译器文档以了解其工作原理(顺便说一下,编译器是g ++)。
不需要。相反,您明确指定数据/通信格式并实现该规范,而不是依赖于C编译器的实现细节。不同的C编译器甚至不会产生相同的输出。

4

为了实现互操作性,可以查看ByteBuffer类。

本质上,您需要创建一个特定大小的缓冲区,将不同类型的变量放置在不同的位置,然后在结束时调用array()方法以检索“原始”数据表示:

ByteBuffer bb = ByteBuffer.allocate(8);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.put(0, someChar);
bb.put(4, someInteger);
byte[] rawBytes = bb.array();

但是你需要自己确定要在哪里放置填充--即在位置之间跳过多少字节。

如果要读取从C语言编写的数据,则通常需要将 ByteBuffer 包装在从文件中读取的某些字节数组周围。

如果有帮助的话,我在ByteBuffer上写了更多内容。


是的,我一直在使用ByteBuffer。我真正遇到的问题是在写入/读取时如何确定要填充多少字节。 - Lehane

2

使用javolution Struct类(请参见http://www.javolution.org)是在Java中读取/写入C结构的方便方法。这不会帮助您自动填充/对齐数据,但它确实使在ByteBuffer中持有的原始数据更加方便地处理。如果您不熟悉javolution,则值得一看,因为其中还有很多其他很酷的东西。


1

这个空洞是可配置的,编译器有开关可以将结构体对齐为1/2/4/8个字节。

因此,第一个问题是:您想要模拟哪种精确的对齐方式?


1

使用Java,数据类型的大小由语言规范定义。例如,byte类型为1个字节,short为2个字节,依此类推。这与C不同,C中每种类型的大小都依赖于体系结构。

因此,重要的是要了解二进制文件的格式,以便能够将文件读入Java。

可能需要采取措施来确保字段具有特定的大小,以解决编译器或体系结构之间的差异。对齐方式的提及似乎意味着输出文件将取决于体系结构。


1

你可以尝试preon

Preon是一个Java库,用于以声明式(基于注释)的方式构建位流压缩数据的编解码器。类似于JAXB或Hibernate,但适用于二进制编码数据。

它可以处理大小端二进制数据、对齐(填充)和各种数字类型等其他功能。这是一个非常不错的库,我非常喜欢它。

我的0.02美元。


1

0

据我理解,您的意思是说您无法控制C程序的输出。您必须将其视为给定。

那么,您是否需要针对某些特定结构读取此文件,还是必须在一般情况下解决此问题?我的意思是,问题是有人说:“这是由X程序创建的文件,您必须用Java读取它”吗?还是他们希望您的Java程序读取C源代码,找到结构定义,然后在Java中读取它?

如果您有一个特定的文件要读取,那么问题并不是很难。通过查看C编译器规范或研究示例文件,找出填充位置。然后,在Java端,将文件作为字节流读取,并构建您知道即将到来的值。基本上,我会编写一组函数从InputStream中读取所需数量的字节,并将它们转换为适当的数据类型。例如:

int readInt(InputStream is,int len)
  throws PrematureEndOfDataException
{
  int n=0;
  while (len-->0)
  {
    int i=is.read();
    if (i==-1)
      throw new PrematureEndOfDataException();
    byte b=(byte) i;
    n=(n<<8)+b;
  }
  return n;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接