[comp.lang.c] near and far in MSC

jcl@bdrc.UUCP (John C. Lusth) (07/29/88)

I'm porting some software, written in C, from a SUN to an IBM PC.
The program is small but does a fair bit of dynamic allocation.
When I compile it under the small memory model, every thing runs fine,
until I increase the amount of allocation the program performs.
Then it hits the 64K code+data limit, and malloc () fails.

I tried recompiling under the compact memory model (small code, large data),
but upon execution, the program behaves strangely.  Specifically,
when I call a varargs function, it can't recognize the arguments
or the end of the argument list.  My call is


    #define LOAD_KB 1

    result = jess (LOAD_KB, "rules", 0);

The code that handles the variable argument list looks like:

    va_start(ap);
    while ((args[argno] = va_arg(ap, char *)) != (char *) 0)
	if (argno < MAX_ARGS-1)
	    ++argno;
	else
	    error (TOO_MANY_ARGS, 
		jess () was given too many arguments (%d)\n", argno);
    va_end(ap);


I think the problem has to with the difference between near and far
pointers (eg. the terminating zero in the call will be interpreted as
a NULL pointer to char by the function jess).  Could someone explain
to me how one programs under memory models other than the small one?

John C. Lusth
Becton Dickinson Research Center
RTP, NC 27709

...!mcnc!bdrc!jcl

chris@mimsy.UUCP (Chris Torek) (07/29/88)

In article <331@bdrc.UUCP> jcl@bdrc.UUCP (John C. Lusth) writes:
[program works in small model, fails in compact]:
>    result = jess (LOAD_KB, "rules", 0);

[part of jess():]
>    while ((args[argno] = va_arg(ap, char *)) != (char *) 0)

>I think the problem has to with the difference between near and far
>pointers ....

You have the right general idea.  The problem is that the call is
wrong, and has been wrong all along.  It is only luck that it works
in the so-called `small model'.

>Could someone explain to me how one programs under memory models
>other than the small one?

By writing the code correctly in the first place:

	result = jess(LOAD_KB, "rules", (char *)0);

And now, two instant replays....
::::::::::::::
save/216
::::::::::::::
Path: mimsy!chris
From: chris@mimsy.UUCP (Chris Torek)
Newsgroups: comp.lang.c
Subject: Why NULL is 0
Summary: you have seen this before, but this one is for reference
Message-ID: <10576@mimsy.UUCP>
Date: 9 Mar 88 02:26:10 GMT
References: <2550049@hpisod2.HP.COM> <7412@brl-smoke.ARPA> <3351@chinet.UUCP> <10574@mimsy.UUCP>
Organization: U of Maryland, Dept. of Computer Science, Coll. Pk., MD 20742
Lines: 73

(You may wish to save this, keeping it handy to show to anyone who
claims `#define NULL 0 is wrong, it should be #define NULL <xyzzy>'.
I intend to do so, at any rate.)

Let us begin by postulating the existence of a machine and a compiler
for that machine.  This machine, which I will call a `Prime', or
sometimes `PR1ME', for obscure reasons such as the fact that it
exists, has two kinds of pointers.  `Character pointers', or objects
of type (char *), are 48 bits wide.  All other pointers, such as
(int *) and (double *), are 32 bits wide.

Now suppose we have the following C code:

 	main()
	{
 		f1(NULL);	/* wrong */
 		f2(NULL);	/* wrong */
 		exit(0);
 	}
 
 	f1(cp) char *cp; { if (cp != NULL) *cp = 'a'; }
 	f2(dp) double *dp; { if (dp != NULL) *dp = 2.2; }

There are two lines marked `wrong'.  Now suppose we were to define NULL
as 0.  Clearly both calls are then wrong: both pass `(int)0', when the
first should be a 48 bit (char *) nil pointer and the second a 32 bit
(double *) nil pointer.

Someone claims we can fix that by defining NULL as (char *)0.  Suppose
we do.  Then the first call is correct, but the second now passes a
48 bit (char *) nil pointer instead of a 32 bit (double *) nil pointer.
So much for that solution.

Ah, I hear another.  We should define NULL as (void *)0.  Suppose we
do.  Then at least one call is not correct, because one should pass
a 32 bit value and one a 48 bit value.  If (void *) is 48 bits, the
second is wrong; if it is 32 bits, the first is wrong.

Obviously there is no solution.  Or is there?  Suppose we change
the calls themselves, rather than the definition of NULL:

	main()
	{
		f1((char *)0);
		f2((double *)0);
		exit(0);
	}

Now both calls are correct, because the first passes a 48 bit (char *)
nil pointer, and the second a 32 bit (double *) nil pointer.  And
if we define NULL with

	#define NULL 0

we can then replace the two `0's with `NULL's:

	main()
	{
		f1((char *)NULL);
		f2((double *)NULL);
		exit(0);
	}

The preprocessor changes both NULLs to 0s, and the code remains
correct.

On a machine such as the hypothetical `Prime', there is no single
definition of NULL that will make uncasted, un-prototyped arguments
correct in all cases.  The C language provides a reasonable means
of making the arguments correct, but it is not via `#define'.

::::::::::::::
save/234
::::::::::::::
Path: mimsy!chris
From: chris@mimsy.UUCP (Chris Torek)
Newsgroups: comp.lang.c
Subject: Re: NULL etc.
Message-ID: <12290@mimsy.UUCP>
Date: 2 Jul 88 20:36:44 GMT
References: <MWlAyzy00hIE82Q1Qc@cs.cmu.edu> <6966@cup.portal.com> <3458@rpp386.UUCP>
Organization: U of Maryland, Dept. of Computer Science, Coll. Pk., MD 20742
Lines: 91

C's untyped nil pointer, which MUST be given a type before it can be
used correctly, is written as `0' (and `0L', and possibly using
constant integer expressions, depending on whose definition you use;
but `0' suffices and must work).

After it has been given a type (`(char *)0') it becomes a nil pointer
of that type.  Once it has a type (if we ignore some fine points in the
dpANS, many of which are unlikely to be implemented in current C
compilers) it may not be used as a nil pointer of another type.  Hence
(char *)0 is a nil pointer to char, and as such may not be used as a
nil pointer to int, or a nil pointer to struct tcurts, or indeed as
anything other than a pointer to char.  It may work---indeed, it is
more likely to work than to fail---but it is incorrect and unportable,
and should (and does in PCC) draw at least a warning from the compiler.

There are only two ways that the untyped nil pointer can acquire a
type, namely assignment and comparison.  Casts are a special case of
assignment, as are arguments to functions that have prototypes in
scope.  Where this causes the most trouble is in arguments to functions
that do not have prototypes in scope, or for which the prototype does
not specify a type for that argument: e.g., execl():

	f() {
		void execl(char *, ...);

		execl("prog", "prog", "arg1", "arg2", ___);
	}

The only correct way to fill in the blank is with (char *)0 (or
possibly (char *)0L and similar tricks; outside of obfuscated C
contests, these tricks are not worth considering).

The dpANS has at present one more instance of an `untyped' nil pointer,
namely `(void *)0'.  This is an anomaly in the type system, and, while
it has some beneficial properties, I believe that overall it makes the
situation worse, not better.  The differences between using `0' and
`(void *)0' as a `generic nil' are, first, that while 0 is also an
integer constant, (void *)0 is not, and second, that (void *)0 is also
a typed nil pointer (ouch!---more below).

Suppose that NULL is defined as either `0' or `(void *)0'---one of the
two untyped nil pointers---but that we do not know which one.  Which of
the following calls are correct?

	/* defintions before the fragments (note lack of prototypes) */
	void f1(cp) char *cp; { <code> }
	void f2(ip) int *ip; { <code> }
	void f3(vp) void *vp; { <code> }

	...
		f1(NULL);		/* call 1 */
		f1((char *)NULL);	/* call 2 */
		f2(NULL);		/* call 3 */
		f2((int *)NULL);	/* call 4 */
		f3(NULL);		/* call 5 */
		f3((void *)NULL);	/* call 6 */

It is easy to see that calls 2, 4, and 6 (which cast their arguments
and hence provide types) are correct.  The surprise is that while calls
1, 3, and 5 are all wrong if NULL is defined as `0', calls 1 and 5 are
both correct, or at least will both work, if NULL is defined as
`(void *)0'.  Call 3 is wrong in any case.

We can get away with `f1((void *)0)' only because of a technicality:
the dpANS says that (void *) and (char *) must have the same
representation (which more or less means `must be the same type'), and
because (void *) is a valid pointer type, (void *)0 must be a valid nil
pointer of type `void *', and thus must also be a valid nil pointer of
type `char *'.  (Actually, this argument glosses over a subsidiary
technicality, in that there is no guarantee that there is only ONE
valid nil pointer of any given type, but that way lies madness.  There
are more arguments about whether `same representation' implies
`indistinguishable'; these, too, are best left untouched.)

There are no ANSI-conformant C compilers, for there is as yet no ANSI C
standard.  One should therefore assume that code may have to run under
a compiler where NULL is defined as `0', not as `(void *)0', and should
therefore avoid calls like 1, 3, and 5 above.
-- 
In-Real-Life: Chris Torek, Univ of MD Comp Sci Dept (+1 301 454 7163)
Domain:	chris@mimsy.umd.edu	Path:	uunet!mimsy!chris

neil@swdev.Waterloo.NCR.COM (Neil A. Law) (07/29/88)

>
>    result = jess (LOAD_KB, "rules", 0);
>
>The code that handles the variable argument list looks like:
>
>    va_start(ap);
>    while ((args[argno] = va_arg(ap, char *)) != (char *) 0)
                         .
                         .
                         .
>
>I think the problem has to with the difference between near and far
>pointers (eg. the terminating zero in the call will be interpreted as
>a NULL pointer to char by the function jess).  Could someone explain
>to me how one programs under memory models other than the small one?
>
>John C. Lusth
>Becton Dickinson Research Center
>RTP, NC 27709
>
>...!mcnc!bdrc!jcl

John, 
    Your problem is definitely in the difference between near and far pointers
and some inconsistency in your coding. First, short pointers are 2 bytes in
length, far pointers are 4. Second, if you analyze your call to "jess" the 
last parameter your are passing it is an int, not a pointer.

When you call the function "jess" and pass it a "0" as the last parameter,
the compiler pushes an int onto the stack (a 2 byte quantity).  When "jess"
computes the while statement, it is looking for a 4 byte quantity (char *).

Since near pointers are the same length as an int in a small model your code
works fine there.

Solution, cast the 0 in the call to jess with a (char *), this way
the compiler will always push sufficient information on the stack.

Hope this helps.

P.S. We had this problem in droves when we converted a 68000 application to
run on the PC.  On the 68K ints and pointers are the same size so comparisons
of "ptr == 0" worked fine on the 68K but failed on the PC in large model.  It
took our developers a long time to track down all of these inconsistencies and
taught us a valuable lesson, casting is important, especially in dealing with
constants.