[comp.lang.c] Prototyping in an imperfect world....

ghoti+@andrew.cmu.edu (Adam Stoller) (06/30/90)

(lengthy - but lot's of white-space  -- sorry)

Some of this is old news, but some may not be - and I'm a bit interested
in some [reasonable] opinions on either....

[prolog (harmonicas playing in the background?)]
As many of us are aware - there is a wonderful thing called prototyping
-- especially with respect to ANSI-C.  Unfortunately we are also aware
that while some people on some platforms may have full prototyping
abilities others may not -- and this tends to cause problems for people
who'd like to try and make their code portable....

There are two different places where this comes into effect:

1) extern reference declarations
2) function definitions

The first I believe has been reasonably well addressed.  For those not
familliar with a way to easily handle it I provide the following
[possible] definition:

	/* external reference declarations */
	/* use: extern int foo P_((int x, char c, long d)); */
	#ifdef __STDC__
	#define P_(args)    args
	#else
	#define P_(args)    ()
	#endif

A hack? Yes, but a farily clean and reasonable one (IMO).

The second situation however I haven't seen addressed much.  About the
only way I've seen people deal with it was by doing something like:

	int
	#ifdef __STDC__
	foo(int x, char c, long d)
	#else
	foo(x, c, d)
	  int x;
	  char c;
	  long d;
	#endif
	{
	   ....
	}

This, IMO, is both a painful (to implement/maintain) and ugly hack - and
one which I'd like to avoid if possible.

To this end - one of the people I work with came up with the idea for a
macro to use for the definitions - which I'd like to share with you now
and get some opinions on:

	#ifdef __STDC__
	#define	PP_(a)	(
	#define	V_	void)
	#define	S_	,
	#define	E_	)
	#else
	#define	PP_(a)	a
	#define	V_
	#define	S_	;
	#define	E_	;
	#endif

This allows the above example to be defined like:

	int foo PP_((x, c, d))
	  int	x S_	
	  char 	c S_
	  long	d E_
	{
	    ......
	}

Or
	int foo PP_(())
	V_
	{
	    ....
	}

Which for the most part is reasonably close to the old-style definitions
though nevertheless hackish.


[Question Time]

Q-1) Is it too ugly for words  ?
	Personally, I admit not liking the loss of the
	commas/semicolons which are pretty much second-nature when
	typing in the code, but other than that it doesn't bother me
	too much.

Q-2) Is it too hackish to be considered reasonable ?
	I think it stays within the boundary of reasonable hacks
	(whatever that boundary may be, unless you're a purist who
	considers any hack evil??)

Q-3) Is it reasonably maintainable ?
	It suffers a little of the same problem as the earlier #ifdef
	example - in that there are still two places where the name and
	number of arguments must be maintained - but I think this one
	wears a little better in that it's really only as bad(?) as the
	old-style definitions.

Q-4) Can it be made better ?
	Mostly I mean this in terms of the symbols chosen for the
	macros, and possibly the macros themselves.  Certainly there
	are tons of alternatives ("use A_ instead of S_", etc) - but
	I'm looking for something a bit more substantial, with
	reasonable explanation. (hey, it's always possible that we
	might have gotten it "right" the first time....:-)
	
	(FYI: V_ == void, S_ == separator, E_ == end-of-list)



[Visualization]
(some blank lines removed from preprocessor output)
[      | <-column 0]
----------------
    > cat foo.c
    /* #define's as shown above */

        int fname PP_((a1, a2))
        int a1 S_
        int a2 E_
        {
        }

        struct lconv *ansi_localeconv PP_(())
        V_
        {
        }

----------------
    > cc -E -D__STDC__ foo.c
        int fname 	(
        int a1 ,
        int a2 )
        {
        }

        struct lconv *ansi_localeconv 	(
        void)
        {
        }

----------------
    > cc -E -U__STDC__ foo.c
        int fname 	(a1, a2)
        int a1 ;
        int a2 ;
        {
        }

        struct lconv *ansi_localeconv 	()

        {
        }

----------------

ghoti+@andrew.cmu.edu (Adam Stoller) (07/01/90)

In reading my message, now posted, I'd like to appologize for some of the formatting - Iforgot to make sure it would appear indented as attended (long story - don't ask ;-)

However, I also think I found a situation not covered under my examples - that is, varargs (...) - so add the following to the defines:


#ifdef __STDC__
#define D_    ...
#else
#define D_
/* or should that be #define D_ va_list ?? or something?? */
#endif


As you can see, this raises a few more questions - again [reasonable] responses
appreciated.

Thanks

--fish

karl@haddock.ima.isc.com (Karl Heuer) (07/04/90)

[Note: To conserve vertical space I've paraphrased heavily and eliminated some
newlines from C code examples; the style questions herein deal with the use of
the macros, not the whitespace convention.  --kwzh]

In article <MaWxonu00as9AA2kM1@andrew.cmu.edu> ghoti+@andrew.cmu.edu (Adam Stoller) writes:
>[Prototypes are good, but it's also good to have code that's sufficiently
>portable that it can be compiled on a pre-ANSI implementation of C.]
>About the only way I've seen people deal with it was by doing something like:
>[#1]
>	#ifdef __STDC__
>	foo(int x, char c, long d)
>	#else
>	foo(x, c, d) int x; char c; long d;
>	#endif
>
>[#2: A proposed solution using a bunch of macros that expand into the correct
>keywords and punctuators in the two C universes:]
>	int foo PP_((x, c, d)) int x S_  char c S_  long d E_ { ... }

Yuck.  In my opinion, #2 is worse than #1; the improvement in maintainability
by having only one set of declarations is more than offset by the pain of
having to use what is, in effect, a new language.  Consider instead...

#3: Use prototypes only for declarations, and continue to use the obsolescent
syntax for definitions (but with a declaring prototype, preferably in a
header, already in scope).
	#ifdef __STDC__
	extern int foo(int, int, long);
	#endif
	int foo(x, c, d) int x; int c; long d; { ... }
Since Classic C can't pass `char' by value, the middle argument should
actually be declared `int' to minimize surprises.

#4: Just use prototypes.  On a system with no ANSI compiler, use
	deproto <foo.c >temp.c; cc -c temp.c; mv temp.o foo.o; rm temp.c
or something equivalent.  (This can be tucked into a makefile, or a shell
script that emulates `cc'.)  This is the approach that I use myself; it also
has the advantage that, when ANSI is ubiquitous and nobody cares about Classic
C anymore, the maintenance programmer won't have to do a thing.

The drawback, of course, is that you have to have a deprotoizer.  One such is
`unprotoize', which hangs off of gcc.  My preference is to maintain one that
is entirely self-contained (it doesn't even require lex), and is short enough
that you could type it in from the hardcopy listing if you had to.

The bad news is that, in order to meet this latter requirement, the language
it accepts is trimmed to the bone.  It doesn't handle arguments of type array
or function, or pointers to such, but the former are useless and the latter
are too rare to be worth the trouble.  A more serious problem is that it's
tuned to my personal coding style.  I hope to get back to it someday and make
it less picky about horizontal whitespace at least, but for now it's provided
as-is.  (Send me any improvements, but keep the program small.)

There's a compile-time debugging option that will make it indicate at what
point the attempted parse failed on each line.

Karl W. Z. Heuer (karl@kelp.ima.isc.com or ima!kelp!karl), The Walking Lint
--------cut here--------
/*
 * deproto 1.00 by Karl Heuer.  public domain.
 *
 * expects a prototype to match one of these two patterns:
 *   decl ::= \t \q*\i(\t` \q+'?`, \t` \q+'?'*`, ...'?);
 *   defn ::= \t \q*\i(`void|\t \q*\i`, \t \q*\i'*'`, ...'?) {
 * `' denotes grouping, | alternation, * Kleene closure, + positive closure,
 * ? option, \i identifier, \t type (single word or known multi-word type,
 * optionally preceded by a storage class); \q type qualifier or literal star;
 * other characters literal.  examples:
 *   int main(int argc, char **argv) {
 *   extern int rand(void);
 *   static int myprintf(char const *fmt, ...) {
 */
#include <stdio.h>
#include <string.h>

#define MAXLINE 2048
#define MAXARGS 32

static char buf[MAXLINE];
static char *larg[MAXARGS], *marg[MAXARGS], *rarg[MAXARGS];

#define isword(c) (c>='a'&&c<='z'||c>='A'&&c<='Z'||c>='0'&&c<='9'||c=='_')
#if defined(DEBUG)
#define doskip(k) { fprintf(stderr, "%d <%.*s|%.*s>\n", k, p-buf, buf, strlen(p)-1, p); goto skip; }
#else
#define doskip(ignore) goto skip
#endif

static int skiptype(pp) register char **pp; {
#define p (*pp)
    static char *known[] = {
	"short int", "long int", "unsigned short int", "unsigned long int",
	"unsigned int", "unsigned char", "unsigned short", "unsigned long",
    };
    register char **k;
    for (k = known; k < known+sizeof(known)/sizeof(char *); ++k) {
	register int len = strlen(*k);
	if (strncmp(p, *k, len) == 0 && !isword(p[len])) {
	    p += len;
	    return (1);
	}
    }
    if (strncmp(p, "struct ", 7) == 0) p += 7;
    else if (strncmp(p, "union ", 6) == 0) p += 6;
    else if (strncmp(p, "enum ", 5) == 0) p += 5;
    if (!isword(*p)) return (0);
    do ++p; while (isword(*p));
    return (1);
#undef p
}

static char *skipstar(p) register char *p; {
    for (;;) {
	if (*p == '*') ++p;
	else if (strncmp(p, "const ", 6) == 0) p += 6;
	else if (strncmp(p, "volatile ", 9) == 0) p += 9;
	else return (p);
    }
}

main() {
    char *p;
    register int i, n;
    register int isdecl, isdefn;
    while (fgets(buf, MAXLINE, stdin) != NULL) {
	p = buf;
	if (strncmp(p, "extern ", 7)==0 || strncmp(p, "static ", 7)==0) p += 7;
	if (!skiptype(&p)) doskip(0);
	if (*p++ != ' ') doskip(1);
	p = skipstar(p);
	if (!isword(*p)) doskip(2);
	do ++p; while (isword(*p));
	if (*p++ != '(') doskip(3);
	n = 0;
	isdecl = isdefn = 1;
	for (;;) {
	    larg[n] = p;
	    if (strncmp(p, "register ", 9) == 0) p += 9;
	    if (!skiptype(&p)) doskip(4);
	    if (*p != ' ') {
		isdefn = 0;
	    } else {
		++p;
		p = skipstar(p);
		if (!isword(*p)) isdefn = 0;
	    }
	    marg[n] = p;
	    while (isword(*p)) ++p;
	    rarg[n++] = p;
	    if (*p != ',' || p[1] != ' ') break;
	    p += 2;
	    if (*p == '.' && p[1] == '.' && p[2] == '.') { p += 3; break; }
	}
	if (*p != ')') doskip(5);
	if (isdecl && (p[1] == ';' || (p[1] == ' ' && p[2] == '{' && strncmp(larg[0], "void)", 5) == 0))) {
	    printf("%.*s%s", larg[0]-buf, buf, p);
	} else if (p[1] == ' ' && p[2] == '{' && isdefn) {
	    printf("%.*s", larg[0]-buf, buf);
	    for (i=0; i<n; ++i) {
		if (i != 0) printf(", ");
		printf("%.*s", rarg[i]-marg[i], marg[i]);
	    }
	    printf(") ");
	    for (i=0; i<n; ++i) printf("%.*s; ", rarg[i]-larg[i], larg[i]);
	    printf("%s", p+2);
	} else {
	skip:
	    printf("%s", buf);
	}
    }
    exit(0);
}

ghoti+@andrew.cmu.edu (Adam Stoller) (07/05/90)

Excerpts from netnews.comp.lang.c: 3-Jul-90 Re: Prototyping in an
imper.. Karl Heuer@haddock.ima.i (6245)

> Yuck.  

In looking at it again - I agree - but for me it's a definite Yuck to
both of the options.

However, I apparently went down this road of hackdom due to a
brain-damaged compiler which whines incessantly when you mix prototype
declarations and old-style definitions (the compiler defines __STDC__
but apparently does not conform [completely] to ANSI specs).

So - having traveled the bumpy road, I come full circle - and will
probably choose to just a simple prototype declaration (which even if
not used, serves as additional documentation for the function) and
old-style definitions  - at least until ANSI compliant
pre-processors/compilers are more prevelant than they are now.

Sorry to have poured such a filthy hack onto your collective screens (;-)

--fish