Home > OS >  Initialize numeric struct fields with char array in C?
Initialize numeric struct fields with char array in C?

Time:01-27

Let's say I'm creating a struct with mostly numeric fields. Then, I'd like to initialize these different fields with values that are ASCII chars, so that in the end, I could cast the struct as a char* pointer, and print it in one go, and obtain a sort of a per-byte "map" printout of the struct.

I came to the following example that compiles:

#include <stdio.h>
#include <string.h>
#include <inttypes.h>

struct mystruct_s {
  uint8_t id_tag[6]; 
  uint8_t fld_one[2];
  uint64_t fld_two;
  uint16_t fld_three;
  uint16_t fld_four;
  uint16_t fld_five;
};

struct mystruct_s myrh = {
  .id_tag = "123456",
  .fld_one = "78",
  .fld_two = (uint64_t) ((uint8_t[8]) { 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T' }),
};

int main() {
  printf("size %zu (%zu) bytes\n", sizeof(myrh), sizeof(struct mystruct_s));
  printf("as string:\n");
  printf("%s\n", (char*)&myrh);
  printf("char 10: '%c'\n", ((char*)&myrh)[9]);
  return 0;
}

... however, when it runs, it produces:

$ gcc -g main.c -o main.exe
$ ./main.exe
size 24 (24) bytes
as string:
12345678`�V
char 10: ''

... whereas I would have expected something like 12345678TTTTTTT (and char 10 being also 'T'). So, clearly my "cast" did not quite work.

So, is there some sort of syntax, where I could initialize a numeric field of a struct, with the values equivalent to a char array? (Note, the above example shows the intent to initialize all bytes in .fld_two to 'T', that is, 0x54 as per ASCII - but I'm ultimately interested in specifying arbitrary strings like this, with varying characters)


Second thing that I found surprising, is that if I add another field initialization:

...
struct mystruct_s myrh = {
  .id_tag = "123456",
  .fld_one = "78",
  .fld_two = (uint64_t) ((uint8_t[8]) { 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T' }),
  .fld_three = (uint16_t) ((uint8_t[2]) { 'E', 'E' }),
};
...

... then building fails with:

gcc -g main.c -o main.exe
main.c:18:16: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
   .fld_three = (uint16_t) ((uint8_t[2]) { 'E', 'E' }),
                ^
main.c:18:16: error: initializer element is not constant
main.c:18:16: note: (near initialization for ‘myrh.fld_three’)

So, how come it did NOT detect an error on this line:

.fld_two = (uint64_t) ((uint8_t[8]) { 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T' }),

... and yet it did find an error on this line:

 .fld_three = (uint16_t) ((uint8_t[2]) { 'E', 'E' }),

... even if both lines are apparently doing the same thing, just with different sizes and values?

CodePudding user response:

(uint64_t) ((uint8_t[8]) { 'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T' })

This creates a compound literal at the same scope as where it is used. Like any array, the uint8_t [8] decays into a pointer to the first element when used in an expression. So you end up casting an address to uint64_t, not the data.

You can solve this with union type punning. Something like:

typedef union
{
  uint8_t  bytes [8];
  uint64_t u64;
} pun_intended;

...

.fld_two = (pun_intended){ .bytes = {'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T'} }.u64,

However that brings another problem, namely that it is no longer a compile-time constant expression. Which should of course ideally be solved by not using sloppy globals in the first place. But in case that is not an option (maybe you need const), you could alternatively change the type of the struct member:

pun_intended fld_two;
...
.fld_two = (pun_intended){ .bytes = {'T', 'T', 'T', 'T', 'T', 'T', 'T', 'T'} },

CodePudding user response:

Well, I sort of managed to do the effect I desired via unions - but there is so much typing and boilerplate, it defeats the purpose (I wanted the syntax in the OP because it appears easy :) )

#include <stdio.h>
#include <string.h>
#include <inttypes.h>

struct mystruct_s {
  uint8_t id_tag[6]; 
  uint8_t fld_one[2];
  uint64_t fld_two;
  uint16_t fld_three;
  uint16_t fld_four;
  uint16_t fld_five;
} __attribute__((packed));
struct mystruct_alt_s {
  unsigned char id_tag[6]; 
  unsigned char fld_one[2];
  unsigned char fld_two[8];
  unsigned char fld_three[2];
  unsigned char fld_four[2];
  unsigned char fld_five[2];
} __attribute__((packed));

union undata {
  struct mystruct_alt_s myo_alt;
  struct mystruct_s myo;
} ;

union undata data = {
  .myo_alt = {
    .id_tag = "123456",
    .fld_one = "78",
    .fld_two = "TTTTTTTT",
    .fld_three = "HH",
    .fld_four = "RR",
    .fld_five = "VV",
  }
};


int main() {
  printf("size %zu (%zu) bytes\n", sizeof(data), sizeof(struct mystruct_s));
  printf("as string:\n");
  printf("%s\n", (char*)&data.myo);
  return 0;
}

This prints:

$ gcc -g main.c -o main.exe
$ ./main.exe
size 22 (22) bytes
as string:
12345678TTTTTTTTHHRRVV

CodePudding user response:

As an alternative to your own answer, you may use the newly learned X Macros to gather the variable definitions and initialization strings in one place:

#include <stdio.h>
#include <string.h>
#include <inttypes.h>

// X macro taking Type, Name, Count, ValueString
#define FIELDS \
  X(uint8_t, id_tag, [6], "123456") \
  X(uint8_t, fld_one, [2], "78") \
  X(uint64_t, fld_two, , "TTTTTTTT") \
  X(uint16_t, fld_three, , "HH") \
  X(uint16_t, fld_four, , "RR") \
  X(uint16_t, fld_five, , "VV")

#define X(T,N,C,V) T N C;
struct mystruct_s {
  FIELDS
} __attribute__((packed));
#undef X

#define X(T,N,C,V) unsigned char N[sizeof(T C)];
struct mystruct_alt_s {
  FIELDS
} __attribute__((packed));
#undef X

union undata {
  struct mystruct_alt_s myo_alt;
  struct mystruct_s myo;
} ;

#define X(T,N,C,V) .N = V,
union undata data = {
  .myo_alt = {
    FIELDS
  }
};
#undef X

int main() {
  printf("size %zu (%zu) bytes\n", sizeof(data), sizeof(struct mystruct_s));
  printf("as string:\n");
  printf("%s\n", (char*)&data.myo);
  return 0;
}
  • Related