Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is va_start (etc.) reentrant?

While making an edit to a class with a long history, I was stymied by a particular habit of the architect of wrapping his va_start -> va_end sequence in a mutex. The changelog for that addition (which was made some 15 years ago, and not revised since) noted that it was because va_start et. all was not reentrant.

I was not aware of any such issues with va_start, as I always thought it was just a macro for some stack-pointer math. Is there something here I'm not aware of? I don't want to change this code if there will be side-effects.

Specifically, the function in question looks a lot like this:

void write(const char *format, ...)
{
    mutex.Lock();
    va_list args;
    va_start(args, format);
    _write(format, args);
    va_end(args);
    mutex.Unlock();
}

This is called from multiple threads.

like image 788
Nate Avatar asked Oct 05 '10 16:10

Nate


People also ask

What is Va_copy?

va_copy() The va_copy() macro copies the (previously initialized) variable argument list src to dest. The behavior is as if va_start() were applied to dest with the same last argument, followed by the same number of va_arg() invocations that was used to reach the current state of src.

What is Va ARG?

va_arg can be used any number of times in the function to retrieve arguments from the list. va_copy makes a copy of a list of arguments in its current state. The src parameter must already be initialized with va_start ; it may have been updated with va_arg calls, but must not have been reset with va_end .

What is Va_list in C?

va_list is a complete object type suitable for holding the information needed by the macros va_start, va_copy, va_arg, and va_end. If a va_list instance is created, passed to another function, and used via va_arg in that function, then any subsequent use in the calling function should be preceded by a call to va_end.


2 Answers

As far as being serially-reentrant (ie., if foo() uses va_start is it safe for foo() to call bar() which also uses va_start), the answer is that's fine - as long as the va_list instance isn't the same. The standard says,

Neither the va_start nor va_copy macro shall be invoked to reinitialize ap without an intervening invocation of the va_end macro for the same ap.

So, you're OK as long as a different va_list (referred to above as ap) is used.

If by reentrant you mean thread-safe (which I assume you are, since mutexes are involved), you'll need to look to the implementation for the specifics. Since the C standard doesn't talk about multi-threading, this issue is really up to the implementation to ensure. I could imagine that it might be difficult to make va_start thread-safe on some oddball or small architectures, but I think if you're working on a modern mainstream platform you're likely to have no problems.

On the more mainstream platforms as long as a different va_list argument is being passed to the va_start macro you should have no problem with multiple threads passing through the 'same' va_start. And since the va_list argument is typically on the stack (and therefore different threads will have different instances) you're generally dealing with different instances of the va_list.

I think that in your example, the mutexes are unnecessary for the varargs use. However, if the write(), it certainly would make sense for a write() call to be serialized so that you don't have multiple write() threads screwing up each other's output.

like image 152
Michael Burr Avatar answered Nov 15 '22 07:11

Michael Burr


Well, the way the variable argument access is implemented in C makes it rather obvious that the va_list objects stores some internal state. That makes it not reentrant, meaning that calling va_start on a va_list object would invalidate the effect of the previous va_start. But even more precisely, C explicitly prohibits invoking va_start again on a va_list object before "closing " the previously invoked va_start session with va_end.

A va_list object is supposed to be used in "non-overlapping" fashion: va_start...va_end. After that you can do another va_start on the same va_list object. But trying to overlap the va_start...va_end sessions on the same va_list object will not work.

P.S. Actually, in theory it is, of course, possible to implement some LIFO-based internal state in any session-based iterator. I.e. it is theoretically possible to allow nested va_start...va_end sessions on the same va_list object (thus making it reentrant in that sense). But C library specification does not provide anything like that.

Note though that in C99 va_list objects are copyable by va_copy. So, if you need to browse the same argument list by several overlapping va_start...va_end sessions, you can always achieve that by creating several independent copies of the original va_list.

P.P.S. Looking at the code sample you provided... There's absolutely no need for any mutexes in this case (as far as the integrity of va_list is concerned). And there's no need for a reentrant va_list object. You code is perfectly fine without any mutexes. It will work fine in multiple-thread environment. Macros from va_... group do not operate on the actual "stack pointer". Instead, they operate on a complete independent va_list object that can be used to iterate over the values stored in the stack. You can think of it as your own, private local stack pointer. Each thread invoking your function will gets its own copy of that va_list iterating over its own stack. There will be no conflict between the threads.

like image 28
AnT Avatar answered Nov 15 '22 07:11

AnT