The best alternative would be to re-factor the code to use if/else. If there truly are thousands of cases it may or may not be very efficient to have a giant case statement in the first place.
However, it because cases could "fall-through" or other odd flow control like Duff's device (I hope not), it may not be completely a straightforward conversion.
It is not likely to be a very good implementation to abuse the preprocessor to "loop". See Writing a while loop in the C preprocessor for sample of what this might look like.
It may be best to write a simple python or awk script. However this approach may also be flawed if the keyword case appears somewhere like a string or if labels the preprocessor changes anything. This may work very well for a narrow one-off conversion though, but without seeing the code in question it is hard to say.
There is a serious problem with either preprocessing approach if the case labels use enumerations. Since enums are still just text strings at the time the preprocessor runs (or an external script), how can it iterate from STATE_10 to STATE_20 without knowing what integers they represent? It can't - the GNU extension really requires compiler support.
If a one-time wholesale replacement of the case statement is too invasive or irregular to manage, you could probably utilize a hybrid approach:
Assuming you have a (notional) example like:
switch(state)
{
case STATE_1:
xxx; break;
case STATE_2 ... STATE_10:
yyy; break;
}
Allocate a previously unused range of indexes. Add one new special index for each existing range label. Use if/else logic to detect the ranges first and then replace the range case with a new standard one. This allows the control flow structure to remain essentially unmodified:
#if !defined(__GNUC__)
#define STATE_RANGE_2_10 101
if(state >= 2 && state <= 10)
state2 = STATE_RANGE_2_10
else if(...)
state2 = STATE_RANGE_x_y
else
state2 = state;
#else /* GNU */
#define STATE_RANGE_2_10 STATE_2 ... STATE_10
state2 = state;
#endif
switch(state2)
{
case STATE_1:
xxx; break;
case STATE_RANGE_2_10:
yyy; break;
}
With some suitable macros this could even be made portable between GNUC and real C if you really wanted GNUC to still use the extension for some reason. Note: I introduced the state2 variable in case it is stored or used outside the local scope. If not, you can skip that.