Wat is de noodzaak van een array met nul elementen?

In de Linux-kernelcode heb ik het volgende gevonden dat ik niet kan begrijpen.

struct bts_action {
         u16 type;
         u16 size;
         u8 data[0];
 } __attribute__ ((packed));

De code is hier: http://lxr.free-electrons.com/source/ include/linux/ti_wilink_st.h

Wat is de behoefte en het doel van een reeks gegevens zonder elementen?


Antwoord 1, autoriteit 100%

Dit is een manier om variabele gegevensgroottes te hebben, zonder malloc(in dit geval kmalloc) twee keer te hoeven aanroepen. Je zou het als volgt gebruiken:

struct bts_action *var = kmalloc(sizeof(*var) + extra, GFP_KERNEL);

Vroeger was dit niet standaard en werd het als een hack beschouwd (zoals Aniket zei), maar het was gestandaardiseerd in C99. Het standaardformaat ervoor is nu:

struct bts_action {
     u16 type;
     u16 size;
     u8 data[];
} __attribute__ ((packed)); /* Note: the __attribute__ is irrelevant here */

Houd er rekening mee dat u geen grootte vermeldt voor het veld data. Merk ook op dat deze speciale variabele alleen aan het einde van de struct kan komen.


In C99 wordt deze kwestie uitgelegd in 6.7.2.1.16 (nadruk van mij):

Als een speciaal geval kan het laatste element van een structuur met meer dan één benoemd lid
een onvolledig arraytype hebben; dit wordt een flexibel arraylid genoemd. In de meeste situaties,
het flexibele arraylid wordt genegeerd. In het bijzonder is de grootte van de structuur alsof de
flexibel array-lid zijn weggelaten, behalve dat het mogelijk meer volgvulling heeft dan
de omissie zou impliceren. Wanneer echter een . (of ->) operator heeft een linker operand die is
(een verwijzing naar) een structuur met een flexibel arraylid en de juiste operandnamen die
lid, gedraagt ​​het zich alsof dat lid is vervangen door de langste array (met dezelfde
elementtype) dat de structuur niet groter zou maken dan het object dat wordt benaderd; de
offset van de array blijft die van het flexibele arraylid, zelfs als dit anders zou zijn
van die van de vervangende array. Als deze array geen elementen zou hebben, gedraagt ​​deze zich alsof
het had één element, maar het gedrag is niet gedefinieerd als er een poging wordt gedaan om daar toegang toe te krijgen
element of om een ​​aanwijzer er voorbij te genereren.

Of met andere woorden, als u het volgende heeft:

struct something
{
    /* other variables */
    char data[];
}
struct something *var = malloc(sizeof(*var) + extra);

Je hebt toegang tot var->datamet indices in [0, extra). Merk op dat sizeof(struct something)alleen de grootte geeft voor de andere variabelen, d.w.z. dataeen grootte van 0 geeft.


Het kan ook interessant zijn om op te merken hoe de standaard voorbeelden geeft van het mallocvan een dergelijk construct (6.7.2.1.17):

struct s { int n; double d[]; };
int m = /* some value */;
struct s *p = malloc(sizeof (struct s) + sizeof (double [m]));

Een andere interessante opmerking van de standaard op dezelfde locatie is (nadruk van mij):

ervan uitgaande dat de aanroep van malloc slaagt, gedraagt ​​het object waarnaar p wordt verwezen zich voor de meeste doeleinden alsof p is gedeclareerd als:

struct { int n; double d[m]; } *p;

(er zijn omstandigheden waarin deze gelijkwaardigheid wordt verbroken; met name de offsets van lid d zijn mogelijk niet hetzelfde).


Antwoord 2, autoriteit 26%

Dit is eigenlijk een hack voor GCC(C90) in feite.

Het wordt ook wel een struct-hackgenoemd.

Dus de volgende keer zou ik zeggen:

struct bts_action *bts = malloc(sizeof(struct bts_action) + sizeof(char)*100);

Het is hetzelfde als zeggen:

struct bts_action{
    u16 type;
    u16 size;
    u8 data[100];
};

En ik kan een willekeurig aantal van dergelijke structobjecten maken.


Antwoord 3, autoriteit 5%

Het idee is om een ​​array van variabele grootte toe te staan ​​aan het einde van de struct. Vermoedelijk is bts_actioneen datapakket met een header van een vaste grootte (de velden typeen size), en datalid. Door het te declareren als een array met een lengte van 0, kan het net als elke andere array worden geïndexeerd. Je zou dan een bts_actionstruct toewijzen, van bijvoorbeeld 1024-byte datagrootte, zoals zo:

size_t size = 1024;
struct bts_action* action = (struct bts_action*)malloc(sizeof(struct bts_action) + size);

Zie ook: http://c2.com/cgi/wiki?StructHack


Antwoord 4, autoriteit 5%

De code is niet geldig C (zie dit). De Linux-kernel is, om voor de hand liggende redenen, helemaal niet bezig met overdraagbaarheid, dus gebruikt hij veel niet-standaard code.

Wat ze doen is een niet-standaard GCC-extensie met arraygrootte 0. Een standaard-compatibel programma zou u8 data[];hebben geschreven en het zou precies hetzelfde hebben betekend. De auteurs van de Linux-kernel houden er blijkbaar van om dingen onnodig ingewikkeld en niet-standaard te maken, als zich een optie voordoet om dit te doen.

In oudere C-standaarden stond het beëindigen van een struct met een lege array bekend als “de struct-hack”. Anderen hebben het doel ervan al uitgelegd in andere antwoorden. De struct-hack, in de C90-standaard, was ongedefinieerd gedrag en kon crashes veroorzaken, vooral omdat een C-compiler vrij is om een ​​willekeurig aantal opvulbytes aan het einde van de struct toe te voegen. Dergelijke opvulbytes kunnen aan het einde van de structuur in botsing komen met de gegevens die u probeerde te “hacken”.

GCC heeft in het begin een niet-standaard extensie gemaakt om dit te veranderen van ongedefinieerd naar goed gedefinieerd gedrag. De C99-standaard heeft dit concept vervolgens aangepast en elk modern C-programma kan deze functie daarom zonder risico gebruiken. Het staat bekend als flexible array memberin C99/C11.


Antwoord 5

Een ander gebruik van een nul-lengte-array is als een benoemd label in een struct om te helpen bij het controleren van de struct-offset van de compileertijd.

Stel dat je een aantal grote structdefinities hebt (die meerdere cacheregels omvatten) waarvan je zeker wilt weten dat ze zijn uitgelijnd met de grens van de cachelijn, zowel in het begin als in het midden waar het de grens overschrijdt.

struct example_large_s
{
    u32 first; // align to CL
    u32 data;
    ....
    u64 *second;  // align to second CL after the first one
    ....
};

In code kun je ze declareren met GCC-extensies zoals:

__attribute__((aligned(CACHE_LINE_BYTES)))

Maar u wilt er toch zeker van zijn dat dit tijdens runtime wordt gehandhaafd.

ASSERT (offsetof (example_large_s, first) == 0);
ASSERT (offsetof (example_large_s, second) == CACHE_LINE_BYTES);

Dit zou werken voor een enkele struct, maar het zou moeilijk zijn om veel structs te behandelen, elk heeft een andere lidnaam die moet worden uitgelijnd. Je zou hoogstwaarschijnlijk een code krijgen zoals hieronder, waar je de namen van het eerste lid van elke struct moet vinden:

assert (offsetof (one_struct,     <name_of_first_member>) == 0);
assert (offsetof (one_struct,     <name_of_second_member>) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, <name_of_first_member>) == 0);
assert (offsetof (another_struct, <name_of_second_member>) == CACHE_LINE_BYTES);

In plaats van deze kant op te gaan, kun je een array met lengte nul declareren in de struct die fungeert als een benoemd label met een consistente naam, maar die geen ruimte in beslag neemt.

#define CACHE_LINE_ALIGN_MARK(mark) u8 mark[0] __attribute__((aligned(CACHE_LINE_BYTES)))
struct example_large_s
{
    CACHE_LINE_ALIGN_MARK (cacheline0);
    u32 first; // align to CL
    u32 data;
    ....
    CACHE_LINE_ALIGN_MARK (cacheline1);
    u64 *second;  // align to second CL after the first one
    ....
};

Dan zou de runtime-bevestigingscode veel gemakkelijker te onderhouden zijn:

assert (offsetof (one_struct,     cacheline0) == 0);
assert (offsetof (one_struct,     cacheline1) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, cacheline0) == 0);
assert (offsetof (another_struct, cacheline1) == CACHE_LINE_BYTES);

Other episodes