Binärdatei mit Perl erzeugen und mit c auslesen

Datenübertragung unabhängig von der Programmiersprache über maschinenlesbare Dateien die nicht XML sind

In Perl ist es schon lange möglich, c-Code einzubinden wie untenstehendes Beispiel zeigt. Um das zu ermöglichen wird das Inline-Modul aufgerufen und entsprechend konfiguriert. Bei der erstmaligen Übergabe dieses Scripts an den Perl-Interpreter wird der auf dem System eingerichete c-Compiler aufgerufen, danach liegt der c-Code als Bytecode in einer DLL-Datei vor. Bei jedem weiteren Aufruf des Perl-Interpreters mit diesem Script wird sämtlicher c-Code nicht erneut kompiliert sondern aus der DLL-Datei geladen.

Untenstehender Code soll jedoch vielmehr auch zeigen, wie Datensequenzen plattformunabhängig gelesen werden können. So wird im gezeigten Beispiel ein gewöhnlicher Perl-Hash nach einem bestimmten Algorithmus in eine Sequenz (Bytefolge, Datei) serialisiert. Der Algorithmus ist einfach: Beim Durchlaufen des assoziativen Arrays wird für jeden Schlüssel und den dazugehörigen Wert die jeweilige Länge ermittelt und als Little Endian in die Sequenz geschrieben. Vermittels der Perl-Funktion pack() mit der Schablone "V" hat somit jede Längenangabe (Offsetangabe) in der Datei diegleiche Länge von genau 4 Byte (32 Bit). Danach werden die Daten selbst in die Datei geschrieben und der Algorithmus wiederholt sich.

Das c-Äquivalent zur pack-Schablone "V" ist der Datentyp uint32_t. Bei der Umkehrung des Serialize-Algorithmus also werden in einem Rutsch gleich beide Offsetangaben für Schlüssel und Wert auf diesem Datentyp gelesen -- und danach die Daten selbst. Danach wiederholt sich der Lesevorgang auf die gleiche Art und Weise solange bis die fread()-Funktion nichts mehr zu lesen hat.

use strict;
use warnings;
use bytes;
use IO::File;
use Inline(
    C    => 'DATA', # Lokation c code
    NAME => 'MyC'   # Name der dll
);

# ein paar Daten als Hash
my %h = (
    name   => 'Pälzer',
    vname  => 'Horst',
    ort    => 'Alzey',
    plz    => '55055',
    age    => '22',
    range  => 'Private',
    region => 'Rheinland-Pfalz',
);

# hash in eine Datei serialisieren
my $fh = IO::File->new;
$fh->open('binfile', O_CREAT|O_RDWR|O_BINARY|O_TRUNC) or die $!;
foreach my $key( sort keys %h){
    # Sicherstellen dass der Wert definiert ist
    my $val = $h{$key} || "";
    # Längenangaben als Little Endian
    $fh->print( pack("VV", length($key), length($val)).$key.$val );
}
$fh->close;

# Datei mit c lesen und Inhalt ausgeben
read_binary('binfile');

__DATA__
__C__
#include <stdint.h>

void read_binary(char *filename){
    /* mode "rb": lesen in binary mode */
    FILE *fp = fopen(filename, "rb");
    if(fp == NULL){
        printf("Error open file %s", filename);
        return;
    }

    /* Puffer für die beiden Längenangaben */
    uint32_t lens[2];

    /* Deserialize Algorithmus anwenden */
    while( fread(lens,4,2,fp) ){
        /* key length and value length */
        int klen = lens[0];
        int vlen = lens[1];

        /* Speicher anfordern */
        char *key = malloc(sizeof(char*) * klen);
        char *val = malloc(sizeof(char*) * vlen);

        /* Bytes entsprechend Längenangaben
           auf die Strings einlesen */
        fread(key,klen,1,fp);
        fread(val,vlen,1,fp);

        /* Strings terminieren */
        key[klen] = 0;
        val[vlen] = 0;

        /* Gelesene Daten ausgeben */
        printf("%-7s => %s\n", key, val);

        /* Speicher wieder freigeben */
        free(key);
        free(val);
    }
    fclose(fp);
}

/* Ausgabe der c-Funktion

age     => 22
name    => Pälzer
ort     => Alzey
plz     => 55055
range   => Private
region  => Rheinland-Pfalz
vname   => Horst

*/

Zum Vergleich und der Vollständigkeit halber der Lese-Algorithmus in Perl. In Sachen Performance ergibt sich überhaupt kein Unterschied zum Algorithmus in c implementiert.

sub rb{
    $fh->open('binfile', O_RDONLY|O_BINARY) or die $!;
    while( read($fh, my $lenb, 8)   ){
        my($klen,$vlen) = unpack "VV", $lenb;
        read($fh, my $key, $klen);
        read($fh, my $val, $vlen);
        # Augabe Schlüssel => Wert analog der c-Funktion
        printf("%-7s => %s\n", $key, $val);
    }
    $fh->close;
}

Abstrakt

Im Zeitalter der MicroController erlebt der von Niklaus Wirth geprägte Dateibegriff als Bytesequenz eine Renaissance. Von daher noch ein paar Anmerkungen bzw. C-Code zum Thema Binary. Nehmen wir z.B. eine Sequenz von 4 Bytes, welche als Little Endian die Zahl 1145258561 verkörpert. Die einzelnen Oktetten können wie folgt ermittelt werden:

#include <stdint.h>

    unsigned int n = 1145258561;
    uint32_t Vax[1]; // 1 Little Endian
    Vax[0] = n;      // weise integer an 1. Element
    Vax[1] = 0;      // Terminierung
    printf("Binary: %s\n",Vax);

    /* Wertigkeiten der 4 Oktetten ermitteln */
    uint8_t oct[4];
    strncpy(oct, (uint8_t*)Vax, 4); // Beachte den Cast
    printf("Octets: %u %u %u %u", oct[0],oct[1],oct[2],oct[3]);


Ausgabe:
Binary: ABCD
Octets: 65 66 67 68

Freilich sieht das in Perl wesentlich einfacher aus:

print unpack "V",pack "CCCC", 65,66,67,68;

/* 4 Oktetten in eine 32-Bit-Zahl umrechnen */
uint8_t abcd[4] = {65,66,67,68};
uint32_t vx[1];

/* Beachte den Cast, 4 Bytes müssen kopiert werden */
strncpy( (uint8_t*)vx, abcd, 4);
printf("%u\n", vx[0]);

Ausgabe:
1145258561

Angabe eines Offset binär in eine Datei schreiben

Eine Länge (Offset) von 1145258561 Bytes sei angenommen. Wir legen diese Angabe auf das erste Element des Datentype uint32_t und schreiben 4 Byte in die Datei, fertig. Die Datei mit einer Länge von 4 Byte ist in diesem Falle lesbar, ABCD steht drin:

    uint32_t vx[1];
    vx[0] = 1145258561;

    FILE *fh; // open for write in binary mode
    fh = fopen("abcd","wb");

    if(fh == NULL){
        printf("Error open file %s", "abcd");
        return 0;
    }

    fwrite(vx,4,1,fh);
    fclose(fh);

Die Datei für den Inhalt dieser Website

Ist nach dem Muster Entity/Attribute/Value (Perl: Hash of Hashes) aufgebaut und hat eine Länge von derzeit 1MB. Sie wird ebenfalls mit Perl erzeugt und immer dann, wenn neue Inhalte hinzukommen, neu erstellt. Untenstehender C-Code zeigt, wie auch diese Datei mit C gelesen werden kann. Zu beachten ist, dass die Offsetangaben in dieser Datei als Big Endian vorliegen, so dass eine Umrechnung erforderlich ist. Die Library winsock.h stellt dafür die Funktion htonl() zur Verfügung.

use strict;
use warnings;
use Inline(
    C    => 'DATA',
    LIBS => ['-lws2_32'],
    NAME => 'MyC'
);

dbf("d:/home/files/dbf.bin");
###########################################################################
__DATA__
__C__
#include <stdint.h>
#include <winsock.h>

void dbf(char *filename){
    /* rb lesen in binary mode */
    FILE *fp = fopen(filename, "rb");
    if(fp == NULL){
        printf("Error open file %s", filename);
        return;
    }

    /* Puffer für die drei Längenangaben */
    uint32_t lens[3];
    unsigned int elen;
    unsigned int alen;
    unsigned int vlen;


    /* Entity Attribute Value */
    char *ent;
    char *att;
    char *val;

    /* Deserialize Algorithmus anwenden */
    while( fread(lens,4,3,fp) ){
        /* entity length, attribute length and value length,
          convert from little to big endian */
        elen = htonl(lens[0]);
        alen = htonl(lens[1]);
        vlen = htonl(lens[2]);

        /* Speicher anfordern */
        ent = malloc(sizeof(char*) * elen);
        att = malloc(sizeof(char*) * alen);
        val = malloc(sizeof(char*) * vlen);

        /* Bytes entsprechend Längen- bzw. Offsetangaben
           auf die Strings einlesen */
        fread(ent,elen,1,fp);
        fread(att,alen,1,fp);
        fread(val,vlen,1,fp);

        /* Strings terminieren */
        ent[elen] = 0;
        att[alen] = 0;
        val[vlen] = 0;

        /* Gelesene Daten ausgeben */
        printf("%s\n%s\n%s\n", ent, att, val);

        /* Speicher wieder freigeben */
        free(ent);
        free(att);
        free(val);
    }
    fclose(fp);
}

Endianness, Byteorder

Wenn mehr als 1 Byte für eine Zahl erforderlich ist, was bei Zahlen größer 255 der Fall ist, kommen diese Begriffe ins Spiel. Wir können uns den Big Endian etwa so vorstellen, dass von rechts nach links die Bytes beschrieben werden:

Zahlenwert 256
Das erste Byte ist voll und
der Übertrag wandert nach links
in das nächste Byte
0.0.1.0
      ^ hier beginnt die Zählung

Bei einem Little Endian ist es genau umgekehrt:

Zahlenwert 256
Das erste Byte ist voll und
der Übertrag wandert nach rechts
in das nächste Byte
0.1.0.0
^ Zählung beginnt hier

Ebenfalls geläufig sind die Begriffe Network-Order (IP-Adressen) für den Big Endian sowie Vax-Order für den Little Endian.


Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. s​os­@rolf­rost.de.