Qual é a necessidade de um array com zero elementos?

122

No código do kernel do Linux, encontrei a seguinte coisa que não consigo entender.

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

O código está aqui: http://lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

Qual é a necessidade e o objetivo de uma matriz de dados com zero elementos?

Jeegar Patel
fonte
Eu não tenho certeza se existe deve ser um de comprimento zero-matrizes ou struct-hack tag ...
hippietrail
@hippietrail, porque frequentemente quando alguém pergunta o que é essa estrutura, eles não sabem que ela é chamada de "membro flexível da matriz". Se o fizessem, poderiam facilmente encontrar sua resposta. Como não o fazem, não podem marcar a pergunta como tal. É por isso que não temos essa tag.
Shahbaz
10
Vote para reabrir. Concordo que isso não foi duplicado, porque nenhuma das outras postagens aborda a combinação de um "truque de estrutura" não-padrão com comprimento zero e o membro C99 de recurso flexível bem definido do C99. Também acho que é sempre benéfico para a comunidade de programação C lançar alguma luz sobre qualquer código obscuro do kernel do Linux. Principalmente porque muitas pessoas têm a impressão de que o kernel do Linux é algum tipo de código C de última geração, por razões desconhecidas. Embora na realidade seja uma bagunça terrível inundada de explorações fora do padrão que nunca devem ser consideradas como um cânone C.
Lundin
5
Não é uma duplicata - não é a primeira vez que vejo alguém fechar uma pergunta desnecessariamente. Também acho que essa pergunta contribui para a base de conhecimento do SO.
Aniket Inge

Respostas:

139

Essa é uma maneira de ter tamanhos variáveis ​​de dados, sem precisar chamar malloc( kmallocnesse caso) duas vezes. Você usaria assim:

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

Isso costumava não ser padrão e era considerado um hack (como disse Aniket), mas era padronizado no C99 . O formato padrão para ele agora é:

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

Observe que você não menciona nenhum tamanho para o datacampo. Observe também que essa variável especial pode vir apenas no final da estrutura.


Em C99, esse assunto é explicado em 6.7.2.1.16 (grifo meu):

Como um caso especial, o último elemento de uma estrutura com mais de um membro nomeado pode ter um tipo de matriz incompleto; isso é chamado de membro flexível da matriz. Na maioria das situações, o membro flexível da matriz é ignorado. Em particular, o tamanho da estrutura é como se o membro da matriz flexível fosse omitido, exceto que ele pode ter mais preenchimento à direita do que a omissão implicaria. No entanto, quando a. O operador (ou ->) possui um operando esquerdo que é (um ponteiro para) uma estrutura com um membro flexível da matriz e o operando direito nomeia esse membro, se comporta como se esse membro fosse substituído pela matriz mais longa (com o mesmo tipo de elemento ) que não tornariam a estrutura maior que o objeto que está sendo acessado; o deslocamento da matriz deve permanecer o do membro flexível da matriz, mesmo que isso seja diferente do da matriz de substituição. Se essa matriz não tiver elementos,

Ou, em outras palavras, se você tiver:

struct something
{
    /* other variables */
    char data[];
}

struct something *var = malloc(sizeof(*var) + extra);

Você pode acessar var->datacom índices em [0, extra). Observe que sizeof(struct something)apenas o tamanho é responsável pelas demais variáveis, ou seja, o datatamanho é 0.


Também pode ser interessante observar como o padrão realmente fornece exemplos de malloctais construções (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]));

Outra observação interessante do padrão no mesmo local é (ênfase minha):

supondo que a chamada ao malloc tenha êxito, o objeto apontado por p se comporta, para a maioria dos propósitos, como se p tivesse sido declarado como:

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

(há circunstâncias em que essa equivalência é quebrada; em particular, as compensações do membro d podem não ser as mesmas ).

Shahbaz
fonte
Para ser claro, o código original na pergunta ainda não é padrão no C99 (nem no C11) e ainda seria considerado um hack. A padronização C99 deve omitir o limite da matriz.
677 MM
O que é [0, extra)?
SS Anne
36

Na verdade, esse é um truque, para o GCC ( C90 ).

Também é chamado de struct hack .

Então, da próxima vez, eu diria:

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

Será equivalente a dizer:

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

E eu posso criar qualquer número desses objetos struct.

Aniket Inge
fonte
7

A idéia é permitir uma matriz de tamanho variável no final da estrutura. Presumivelmente, bts_actionhá alguns pacotes de dados com um cabeçalho de tamanho fixo (os campos typee size) e um datamembro de tamanho variável . Ao declará-lo como uma matriz de comprimento 0, pode ser indexado como qualquer outra matriz. Você bts_actionalocaria uma estrutura, com datatamanho de 1024 bytes , da seguinte forma:

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

Veja também: http://c2.com/cgi/wiki?StructHack

sheu
fonte
2
@Aniket: Não tenho muita certeza de onde vem essa ideia.
Sheu
em C ++ sim, em C, não é necessário.
8183 amc
2
@sheu, vem do fato de que seu estilo de escrever mallocfaz com que você se repita várias vezes e, se é que actionmuda o tipo de alteração, precisa corrigi-lo várias vezes. Compare os dois a seguir e você saberá: struct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing));vs. struct some_thing *variable = malloc(10 * sizeof(*variable));O segundo é mais curto, mais limpo e claramente mais fácil de mudar.
Shahbaz 12/02
5

O código não é válido C ( veja isso ). O kernel do Linux, por razões óbvias, não se preocupa nem um pouco com a portabilidade, por isso usa bastante código não-padrão.

O que eles estão fazendo é uma extensão não-padrão do GCC com tamanho de matriz 0. Um programa compatível com o padrão teria sido escrito u8 data[];e significaria o mesmo. Os autores do kernel Linux aparentemente adoram tornar as coisas desnecessariamente complicadas e não padronizadas, se uma opção para isso se revelar.

Nos padrões C mais antigos, o final de uma estrutura com uma matriz vazia era conhecido como "o truque da estrutura". Outros já explicaram seu objetivo em outras respostas. O truque de estrutura, no padrão C90, era um comportamento indefinido e poderia causar falhas, principalmente porque um compilador C é livre para adicionar qualquer número de bytes de preenchimento no final da estrutura. Esses bytes de preenchimento podem colidir com os dados que você tentou "invadir" no final da estrutura.

No início, o GCC fez uma extensão não padrão para alterar esse comportamento de indefinido para bem definido. O padrão C99 adaptou esse conceito e qualquer programa C moderno pode, portanto, usar esse recurso sem risco. É conhecido como membro flexível da matriz em C99 / C11.

Lundin
fonte
3
Duvido que "o kernel do linux não esteja preocupado com portabilidade". Talvez você tenha significado portabilidade para outros compiladores? É verdade que está bastante entrelaçado com os recursos do gcc.
Shahbaz
3
No entanto, acho que esse pedaço de código em particular não é um código convencional e provavelmente é deixado de fora porque seu autor não prestou muita atenção a ele. A licença diz que é sobre alguns drivers da texas instruments, por isso é improvável que os principais programadores do kernel prestem atenção a ele. Tenho certeza de que os desenvolvedores do kernel estão atualizando constantemente o código antigo de acordo com novos padrões ou novas otimizações. É grande demais para garantir que tudo esteja atualizado!
Shahbaz
1
@ Shahbaz Com a parte "óbvia", eu quis dizer portabilidade para outros sistemas operacionais, o que naturalmente não faria sentido. Mas eles também não parecem se importar com a portabilidade para outros compiladores, eles usaram tantas extensões GCC que o Linux provavelmente nunca será portado para outro compilador.
Lundin
3
@Shahbaz Como no caso de qualquer coisa rotulada como Texas Instruments, a própria TI é notória por produzir o código C mais inútil, ruim e ingênuo já visto, em suas notas de aplicativos para vários chips de TI. Se o código se originar da TI, todas as apostas relacionadas à chance de interpretar algo útil a partir dele serão desativadas.
Lundin
4
É verdade que o linux e o gcc são inseparáveis. O kernel Linux também é bastante difícil de entender (principalmente porque um sistema operacional é complicado de qualquer maneira). O que quero dizer, porém, é que não é legal dizer "Os autores do kernel Linux aparentemente adoram tornar as coisas desnecessariamente complicadas e não padronizadas, se uma opção para isso se revelar" devido a uma prática ruim de codificação de terceiros .
Shahbaz
1

Outro uso da matriz de comprimento zero é como um rótulo nomeado dentro de uma estrutura para auxiliar na verificação do deslocamento da estrutura do tempo de compilação.

Suponha que você tenha algumas definições grandes de estrutura (que abrangem várias linhas de cache) que deseja garantir que elas estejam alinhadas ao cache da fronteira da linha, tanto no começo quanto no meio em que ela cruza a fronteira.

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

No código, você pode declará-las usando extensões do GCC, como:

__attribute__((aligned(CACHE_LINE_BYTES)))

Mas você ainda deseja garantir que isso seja imposto em tempo de execução.

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

Isso funcionaria para uma única estrutura, mas seria difícil cobrir muitas estruturas, cada uma com um nome de membro diferente a ser alinhado. Você provavelmente obteria código como abaixo, onde você precisa encontrar os nomes do primeiro membro de cada estrutura:

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

Em vez de seguir esse caminho, você pode declarar uma matriz de comprimento zero na estrutura, atuando como um rótulo nomeado com um nome consistente, mas não consome nenhum espaço.

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

Então, o código de asserção de tempo de execução seria muito mais fácil de manter:

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);
Wei Shen
fonte
Idéia interessante. Apenas uma observação de que matrizes de tamanho 0 não são permitidas pelo padrão, portanto, isso é algo específico do compilador. Além disso, pode ser uma boa idéia citar a definição do gcc do comportamento de matrizes de comprimento 0 em uma definição de struct, no mínimo para mostrar se poderia introduzir preenchimento antes ou depois da declaração.
precisa