Working with variadic functions in own fscanf

Viewed 50

So the context here is that I'm working on an own implementation of vfscanf. I currently have the following two function signatures:

int k_fscanf_l(uint32 HA, FILE *stream, int max_length, char *format_tok, int *bytes_moved, void *va_list_item)

int k_vfscanf(uint32 HA, FILE *stream, const char *format, va_list args)

k designates kernel, v designates variable length, and the l is just my flavor of string length-accounting variant.

k_fscanf_l is a helper function that handles a bunch of the heavy duty work for k_vfscanf. In terms of what each function does specifically:

k_vfscanf takes a format string and breaks it down into singular tokens and individually passes them down to k_fscanf_l, or in the case of characters to a separate k_fread function (don't... ask). Inside k_fscanf_l I call sscanf(lstrPtr, mod_tok, va_list_item, &offset) to do most of the heavy work for me, I handle the file stuff aside from that myself.

There's a lot more I could say to over-describe this but I believe it's simpler to get to the crux of the question, which has to do with the last argument in each function. My program is currently asserting from indexing into va_list args, so I wanted to ask:

Questions:

  1. Is it strictly incorrect to index into va_list args like so: (void *)args[va_list_index] and pass that along to k_fscanf_l?
  2. Should I be using the predefined macros instead of void pointers to achieve what I want?
  3. Is it even possible to achieve what I want? That is, can I divvy up the va_list args as multiple void *va_list_item pointers to be filled by sscanf inside the helper function? Do I have to change the function signature?
  4. Do I have specify the types if I call the va_arg macro or is there a (void *)-esque solution?
  5. If not answered above, what would you recommend in this situation?

I currently do the tokenization of the format string and calls to the helper function in the same loop, but I could break those into 2 passes instead to know strictly how many iterations I will do before working with va_list, I just don't know if I can pass the individual items down to be filled even if I do that.

Edit 2: Updating the snippet to be more encompassing

else if (tok_len > 1)
   {
      // Character(s) - Deterministic length and compatibility with byte characters
      if (ft_low == 'c')
      {
         int num_chars;
         ret_val = sscanf(formatTok, "%d", &num_chars);
         if (ret_val < 1)
            num_chars = 1;
         k_print_message(HA, NOTE, 0, "vfscanf", "\tGot %d characters for specifier\n", num_chars);
         
         ret_val = k_fread(HA, va_arg(args, void *), num_chars, 1, stream);
      }

      // %N case - Does not need to actually call scanning function
      else if (ft_low == 'n')
      {
         int *va_list_count = va_arg(args, int *);
         *va_list_count = byte_offset;
      }

      // Strings - Hard to predict strings and scansets, support max length available for buffer
      else if (ft_low == 's' || ft_low == ']')
         ret_val = k_fscanf_l(HA, stream, MAX_LSTR_IN, formatTok, &temp_offset, va_arg(args, char *));
      
      // Non-Floating Numbers - Common bases shouldn't reach 16 digits, ever
      else if (ft_low == 'i' || ft_low == 'd' || ft_low == 'u' || ft_low == 'o' || ft_low == 'x' || ft_low == 'p')
         ret_val = k_fscanf_l(HA, stream, 64, formatTok, &temp_offset, va_arg(args, int *));

      // Floating Point Numbers - resolutions should never reach 64 digits, ever
      else if (ft_low == 'f' || ft_low == 'e' || ft_low == 'g' || ft_low == 'a')            
         ret_val = k_fscanf_l(HA, stream, 64, formatTok, &temp_offset, va_arg(args, float *));

      // Updates value
      byte_offset += temp_offset;
      if (ret_val > 0)
         stored_items++;
      else if (ret_val < 0)
         break;
   }

   k_print_message(HA, NOTE, 0, "vfscanf", "\tGot ft_low: %c\n", ft_low);
   k_print_message(HA, NOTE, 0, "vfscanf", "\tGot ret_val: %d\n", ret_val);

   // Resets flag
   null_flag = 0;

   // Deallocates memory
   kernel_free(HA, formatTok);
}

// Closes va_list from iteration
va_end(args);
1 Answers
  1. Is it strictly incorrect to index into va_list args like so: (void *)args[va_list_index] and pass that along to k_fscanf_l?

It is strictly outside the bounds of the semantics defined by the C language specification to do so. As far as C itself is concerned, therefore, the behavior of doing that is undefined. If your C implementation happens to document semantics for such usage, and if you are not concerned with portability to other C implementations, then that might not be a concern for you. Otherwise, that approach is at best unwise.

  1. Should I be using the predefined macros instead of void pointers to achieve what I want?

The va_start, va_arg, va_end, and va_copy macros are the only means documented by the C language specification for working with va_lists. If your implementation documents additional features for the purpose then it's reasonable to consider using them (thereby incurring a portability cost). If not, then it is unwise to attempt to use anything other than the macros.

  1. Is it even possible to achieve what I want? That is, can I divvy up the va_list args as multiple void *va_list_item pointers to be filled by sscanf inside the helper function? Do I have to change the function signature?

Independent of the specifics of variadic argument handling, you cannot perform such a split if your function is to support POSIX-style %n$ input directives, as these disconnect the order in which directives appear from the order in which the corresponding pointers appear. But that may not be a concern for you.

I also think that trying to use sscanf in support of scanning data from files presents some significant challenges entirely separate from argument interpretation. But I'll suppose you have that part sorted.

As far as obtaining destination pointers from the va_list, you do have the advantage that for valid calls to this specific function, all the variable arguments must be object pointers. If you can rely on all pointer-to-object types to have the same representation, then you can probably get away with extracting the variadic arguments via a series of evaluations such as ...

    void *va_list_item = va_arg(args, void *);

That might be sufficient for your purpose. HOWEVER, C explicitly does not define the behavior of that approach when the actual argument is neither a pointer to void nor a pointer to a character type, and that is at best an uncomfortable situation to put yourself in.

If you want to conform strictly to C then you have to parse the format string at least enough to determine the correct (pointer) type of each argument, and use that type to extract the argument from the va_list. Having done so, you can safely convert the result to type void * to pass on to your helper function, and such conversions are even automatic.

  1. Do I have specify the types if I call the va_arg macro or is there a (void *)-esque solution?

See above.

  1. If not answered above, what would you recommend in this situation?

I am a fan of strict conformance, so in general, I would recommend extracting the variable arguments using their correct types. This presents the least likelihood of unpleasant surprises, though it does mean that you would have to duplicate some of the work that sscanf already has to do.

On the other hand, if there were some kind of general development policy for the project that it's ok to break strict aliasing by treating object pointer types as interchangeable (not merely interconvertible), then there is little additional harm done by relying on that policy by pretending that the variable arguments are all of type void *.

Related