Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]


Groups > comp.lang.c > #101454 > unrolled thread

Function pointers conversions and UB

Started byNoob <root@127.0.0.1>
First post2017-01-11 21:26 +0100
Last post2017-01-12 13:45 +0000
Articles 14 — 7 participants

Back to article view | Back to comp.lang.c


Contents

  Function pointers conversions and UB Noob <root@127.0.0.1> - 2017-01-11 21:26 +0100
    Re: Function pointers conversions and UB "James R. Kuyper" <jameskuyper@verizon.net> - 2017-01-11 15:57 -0500
      Re: Function pointers conversions and UB supercat@casperkitty.com - 2017-01-11 14:37 -0800
    Re: Function pointers conversions and UB jt@toerring.de (Jens Thoms Toerring) - 2017-01-11 21:46 +0000
    Re: Function pointers conversions and UB Keith Thompson <kst-u@mib.org> - 2017-01-11 13:51 -0800
      Re: Function pointers conversions and UB "James R. Kuyper" <jameskuyper@verizon.net> - 2017-01-11 17:06 -0500
    Re: Function pointers conversions and UB Noob <root@127.0.0.1> - 2017-01-12 00:29 +0100
      Re: Function pointers conversions and UB Richard Heathfield <rjh@cpax.org.uk> - 2017-01-11 23:37 +0000
        Re: Function pointers conversions and UB supercat@casperkitty.com - 2017-01-11 21:49 -0800
          Re: Function pointers conversions and UB Richard Heathfield <rjh@cpax.org.uk> - 2017-01-12 09:41 +0000
        Re: Function pointers conversions and UB Noob <root@127.0.0.1> - 2017-01-12 10:52 +0100
          Re: Function pointers conversions and UB Richard Heathfield <rjh@cpax.org.uk> - 2017-01-12 09:58 +0000
    Re: Function pointers conversions and UB Noob <root@127.0.0.1> - 2017-01-12 13:08 +0100
      Re: Function pointers conversions and UB Ben Bacarisse <ben.usenet@bsb.me.uk> - 2017-01-12 13:45 +0000

#101454 — Function pointers conversions and UB

FromNoob <root@127.0.0.1>
Date2017-01-11 21:26 +0100
SubjectFunction pointers conversions and UB
Message-ID<o564ak$fjf$1@dont-email.me>
Hello,

Consider the following code:

void foo(struct s *arg) { }
void (*fp)(void *) = foo;

As far as I understand, this does /not/ have UB, because fp is not used
to call the function. Is that correct?

C99 6.3.2.3 p8
"A pointer to a function of one type may be converted to a pointer to a function of another
type and back again; the result shall compare equal to the original pointer. If a converted
pointer is used to call a function whose type is not compatible with the pointed-to type,
the behavior is undefined."

So the following code does have UB, correct?

void foo(struct s *arg) { }
void (*fp)(void *) = foo;
void bar(void) { fp(); } /* nasal demons from fp() */

I assume the behavior will be "it works as expected" on most platforms.
How/where will it blow up?

Platforms with different argument passing for void * and other pointers?

I lack imagination... What else? Pointer representation/encoding?

Regards.

[toc] | [next] | [standalone]


#101462

From"James R. Kuyper" <jameskuyper@verizon.net>
Date2017-01-11 15:57 -0500
Message-ID<711a0535-e8c0-d5a2-10af-b477f750d671@verizon.net>
In reply to#101454
On 01/11/2017 03:26 PM, Noob wrote:
> Hello,
>
> Consider the following code:
>
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
>
> As far as I understand, this does /not/ have UB, because fp is not used
> to call the function. Is that correct?

No, that's a constraint violation. fp has the type void (*)(void*), 
while foo is implicitly converted to a pointer to foo with the type 
void(*)(struct s*). Those are not compatible types. This problem could 
have been avoided by simply inserting a cast:

void (*fp)(void *) = (void(*)(void*))foo;

With that change, there's absolutely nothing wrong with the code (so far).

> C99 6.3.2.3 p8
> "A pointer to a function of one type may be converted to a pointer to a function of another
> type and back again; the result shall compare equal to the original pointer. If a converted
> pointer is used to call a function whose type is not compatible with the pointed-to type,
> the behavior is undefined."
>
> So the following code does have UB, correct?
>
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
> void bar(void) { fp(); } /* nasal demons from fp() */
>
> I assume the behavior will be "it works as expected" on most platforms.

That depends entirely upon whether what you "expect" is the same as what 
that code actually does on that platform. I recommend against having any 
specific expectations - if your expectations for a given platform happen 
to be right, that will only serve to mislead you into thinking you're 
justified in expecting some particular kind of behavior.

> How/where will it blow up?
>
> Platforms with different argument passing for void * and other pointers?

Yes.

> I lack imagination... What else? Pointer representation/encoding?

That's also a possibility. In the most extreme case (and this is 
something that comes up on some real-world machines), void* and struct 
s* can have different sizes. I hope you can imagine how badly that could 
fail.

The fact that the behavior is undefined if your code makes such a call 
gives implementations permission to optimize code in ways that depend 
upon the assumption that it will not be making such a call. Those 
optimizations can malfunction if you actually do make such a call, and 
the way in which they malfunction is likely to be a way you couldn't 
even imagine.

I don't have any particular optimizations in mind when I write that. I 
just know that similar issues with other kinds of undefined behavior 
have caused malfunctions in code that the author was certain could not 
possibly happen. I would not recommend being certain about this code.

[toc] | [prev] | [next] | [standalone]


#101476

Fromsupercat@casperkitty.com
Date2017-01-11 14:37 -0800
Message-ID<89b8d979-6a89-4836-857f-93d45ba8519f@googlegroups.com>
In reply to#101462
On Wednesday, January 11, 2017 at 2:57:52 PM UTC-6, James R. Kuyper wrote:
> That depends entirely upon whether what you "expect" is the same as what 
> that code actually does on that platform. I recommend against having any 
> specific expectations - if your expectations for a given platform happen 
> to be right, that will only serve to mislead you into thinking you're 
> justified in expecting some particular kind of behavior.

On the vast majority of platforms, the representations of pointer types are
documented, pointers to all kinds of objects have the same representation
as identically-qualified pointers to void, and the only qualifiers which
affect representation are implementation-defined extensions.

On such a platform, there is exactly one sensible behavior if the address
of a function accepting a void* is cast to a pointer to a function accepting
some other pointer type.  The Standard refrains from mandating such behavior
because such a mandate would make no sense on platforms where pointer
representations were dependent upon the target type, but that doesn't mean
that any useful purpose is served by the notion that quality implementations
for platforms which specify the use common pointer representations should
not also specify behaviors of pointers to functions whose arguments have the
same representations as likewise being interchangeable.

I'm well aware that it has become fashionable to say that compilers should
treat code which has a natural meaning on some platforms but not others
as though it would be meaningless on all platforms, and programmers need
to be aware of the need to affirmatively block compilers from doing so, but
in cases where there is one near meaning for a piece of code, there is no
basis for claiming that it's unclear what the programmer would be expecting.

[toc] | [prev] | [next] | [standalone]


#101468

Fromjt@toerring.de (Jens Thoms Toerring)
Date2017-01-11 21:46 +0000
Message-ID<ednnehFgetrU1@mid.uni-berlin.de>
In reply to#101454
Noob <root@127.0.0.1> wrote:
> Consider the following code:

> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;

> As far as I understand, this does /not/ have UB, because fp is not used
> to call the function. Is that correct?

> C99 6.3.2.3 p8
> "A pointer to a function of one type may be converted to a pointer to a
> function of another type and back again; the result shall compare equal to
> the original pointer. If a converted pointer is used to call a function
> whose type is not compatible with the pointed-to type, the behavior is
> undefined."

> So the following code does have UB, correct?

> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
> void bar(void) { fp(); } /* nasal demons from fp() */

This call probaly will get the compiler to complain since
you call via 'fp' without an argument eventhough 'fp' is
defined to point to a function that expects one (regard-
less of if it's the wrong type). I assume you forgot e.g.
a 'NULL' argument here.

> I assume the behavior will be "it works as expected" on most platforms.
> How/where will it blow up?

> Platforms with different argument passing for void * and other pointers?

Indeed. A platform definitely may have pointers of different
sizes, where void pointers, of course, must be large enough
to represent pointers of all possible sizes. One, though pu-
rely theoretical, problem might be an architecture with dedi-
cated registers for e.g. small and large pointers. Given a
calling convention that allows for passing arguments via re-
gisters (which is very common), in that case the argument you
call the function via 'fp' with might be passed via a register
for 'long' pointers, but the function expects it in a register
for a 'short' pointer, thus getting some garbage. Or, if 'small'
pointers have just half the size of a 'large' pointer, it may
try to pass two 'small' pointers combined into a register for
a 'large' pointer, shifting one of them to the more signifi-
cant bits. If the calling convention calls for the first poin-
ter passed to always be shifted up (even if there's only one),
the function called will only use the upper half of the regis-
ter, but which, due to being called with a 'long' pointer, con-
tains the "relevant" bits in the lower half.

This is indeed purely theoretical (at least I don't know of
any such architecture), but that kind of problem isn't unknown
on archtiectures with dedicated data and address registers when
you try to use e.g. ints (or longs, whatever fits the size of a
pointer) instead of a pointer. If you'd do something like this

void foo(void *);
void (*fp)(int) = foo;
...
fp(0x1000);

(where 0x1000 might be a perfectly valid address for the
purposes of foo()) the compiler may pass the argument via
a data register while the actual function called may look
for it in an address register - which just contains some
more or less random value.

On that kind of architecture it's also a not too uncommon (and
hard to spot) bug when you have e.g.

/* no include of <stdlib.h> */

int *ip = (int *) malloc(100 * sizeof *ip);

malloc() will return via a address register, but since
there's no declaration of malloc() the compiler assumes
that it returns an int (at least with C89). And thus in
the caller an attempt will be made to retrieve the return
value from a data register and use that (random) value for
'ip' - with the somewhat unintuitive result that the return
value looks like malloc() succeeded (it's not necessarily
NULL), but an attempt to use 'ip' is rather likely to pro-
duce a segmentation fault.

                      Best regards, Jens
-- 
  \   Jens Thoms Toerring  ___      jt@toerring.de
   \__________________________      http://toerring.de

[toc] | [prev] | [next] | [standalone]


#101470

FromKeith Thompson <kst-u@mib.org>
Date2017-01-11 13:51 -0800
Message-ID<lna8axp8nv.fsf@kst-u.example.com>
In reply to#101454
Noob <root@127.0.0.1> writes:
> Hello,
>
> Consider the following code:
>
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
>
> As far as I understand, this does /not/ have UB, because fp is not used
> to call the function. Is that correct?

It violates a constraint.  There is no implicit conversion from one
pointer-to-function type to another.

> C99 6.3.2.3 p8
> "A pointer to a function of one type may be converted to a pointer to a function of another
> type and back again; the result shall compare equal to the original pointer. If a converted
> pointer is used to call a function whose type is not compatible with the pointed-to type,
> the behavior is undefined."

Yes, it "may be converted", but a cast is required.

This is valid and exhibits no undefined behavior:

    void foo(struct s *arg) { }
    void (*fp)(void *) = (void(*)(void*))foo;

(Presumably `struct s` is defined somewhere; otherwise it's not visible
outside the function definition, and it's going to be difficult to write
a meaningful call.)

> So the following code does have UB, correct?
>
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;

Again, assume a cast.

> void bar(void) { fp(); } /* nasal demons from fp() */

Actually that's another constraint violation -- but
    fp(NULL);
is legal (doesn't violate a syntax rule or constraint) and has undefined
behavior.

> I assume the behavior will be "it works as expected" on most platforms.
> How/where will it blow up?
>
> Platforms with different argument passing for void * and other pointers?
>
> I lack imagination... What else? Pointer representation/encoding?

A likely cause of the call not "working as expected" (which actually
depends a great deal on your expectations) is that you've passed
a pointer to an object that isn't properly aligned to point to a
`struct s` object.  Differing representations and argument passing
conventions are also possibilities (likely not to apply to most
current systems).  And compiler optimization could mess things up.

-- 
Keith Thompson (The_Other_Keith) kst-u@mib.org  <http://www.ghoti.net/~kst>
Working, but not speaking, for JetHead Development, Inc.
"We must do something.  This is something.  Therefore, we must do this."
    -- Antony Jay and Jonathan Lynn, "Yes Minister"

[toc] | [prev] | [next] | [standalone]


#101472

From"James R. Kuyper" <jameskuyper@verizon.net>
Date2017-01-11 17:06 -0500
Message-ID<c4692699-b580-5d0d-a381-dc22675719f3@verizon.net>
In reply to#101470
On 01/11/2017 04:51 PM, Keith Thompson wrote:
> Noob <root@127.0.0.1> writes:
...
>> void foo(struct s *arg) { }
>> void (*fp)(void *) = foo;
>
> Again, assume a cast.
>
>> void bar(void) { fp(); } /* nasal demons from fp() */
>
> Actually that's another constraint violation -- but
>     fp(NULL);
> is legal (doesn't violate a syntax rule or constraint) and has undefined
> behavior.

Actually, judging from his "works as expected" question, if he had 
realized the problem with the missing argument, he might have written 
something like the following:

     struct s s1;
     fp(&s1);


>> I assume the behavior will be "it works as expected" on most platforms.
>> How/where will it blow up?
...
> A likely cause of the call not "working as expected" (which actually
> depends a great deal on your expectations) is that you've passed
> a pointer to an object that isn't properly aligned to point to a
> `struct s` object.

That, at least, wouldn't be a problem with fp(&s1).

[toc] | [prev] | [next] | [standalone]


#101488

FromNoob <root@127.0.0.1>
Date2017-01-12 00:29 +0100
Message-ID<o56f1m$njp$1@dont-email.me>
In reply to#101454
On 11/01/2017 21:26, Noob wrote:

> Consider the following code:
> 
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;

Several of you have pointed out that omitting the cast is
a constraint violation. Are implementations still allowed
to produce object code, after having printed some kind of
warning?

foo.c:3:22: warning: initialization from incompatible pointer type [enabled by default]
 void (*fp)(void *) = foo;
                      ^

> As far as I understand, this does /not/ have UB, because fp is not used
> to call the function. Is that correct?
> 
> C99 6.3.2.3 p8
> "A pointer to a function of one type may be converted to a pointer to a function of another
> type and back again; the result shall compare equal to the original pointer. If a converted
> pointer is used to call a function whose type is not compatible with the pointed-to type,
> the behavior is undefined."
> 
> So the following code does have UB, correct?
> 
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
> void bar(void) { fp(); } /* nasal demons from fp() */

Doh! I did mean fp(0);

> I assume the behavior will be "it works as expected" on most platforms.

"As expected" meant "calling foo with arg=NULL" of course.
(I thought it would be obvious what the expectation was.)

> How/where will it blow up?
> 
> Platforms with different argument passing for void * and other pointers?
> 
> I lack imagination... What else? Pointer representation/encoding?

I may have oversimplified my example. The actual situation
looks more like atexit. One registers a function that is
supposed to be called later, in specific circumstances.

The actual code looks like this:

struct foo { int i; };
extern struct foo *alloc_foo(void);
extern void api_func(struct foo *arg);
extern void register_func(void (*func)(void *), void *data);
/* register_func saves func and data, to be able to call func(data) later */
void user_code(void) {
	struct foo *data = alloc_foo();
	register_func((void (*)(void *))api_func, data);
}


A simple solution would be declaring a wrapper around api_func:

void wrapper(void *arg) { api_func(arg); }

and passing the wrapper to register_func.

This is 100% portable, right?

Regards.

[toc] | [prev] | [next] | [standalone]


#101489

FromRichard Heathfield <rjh@cpax.org.uk>
Date2017-01-11 23:37 +0000
Message-ID<o56fha$p3m$1@dont-email.me>
In reply to#101488
On 11/01/17 23:29, Noob wrote:
> On 11/01/2017 21:26, Noob wrote:
>
>> Consider the following code:
>>
>> void foo(struct s *arg) { }
>> void (*fp)(void *) = foo;
>
> Several of you have pointed out that omitting the cast is
> a constraint violation. Are implementations still allowed
> to produce object code, after having printed some kind of
> warning?

Yes, absolutely. The diagnostic message is a requirement on the 
implementation, but it is not under any obligation to stop translation.

<snip>

> I may have oversimplified my example. The actual situation
> looks more like atexit. One registers a function that is
> supposed to be called later, in specific circumstances.
>
> The actual code looks like this:
>
> struct foo { int i; };
> extern struct foo *alloc_foo(void);
> extern void api_func(struct foo *arg);
> extern void register_func(void (*func)(void *), void *data);
> /* register_func saves func and data, to be able to call func(data) later */
> void user_code(void) {
> 	struct foo *data = alloc_foo();
> 	register_func((void (*)(void *))api_func, data);
> }
>
>
> A simple solution would be declaring a wrapper around api_func:
>
> void wrapper(void *arg) { api_func(arg); }
>
> and passing the wrapper to register_func.
>
> This is 100% portable, right?

FWIW, I can't see anything wrong with it. It's a shame you can't come up 
with a consistent declaration for the registered functions, though, as 
it would enable you to avoid an ugly cast.

-- 
Richard Heathfield
Email: rjh at cpax dot org dot uk
"Usenet is a strange place" - dmr 29 July 1999
Sig line 4 vacant - apply within

[toc] | [prev] | [next] | [standalone]


#101497

Fromsupercat@casperkitty.com
Date2017-01-11 21:49 -0800
Message-ID<ce2dc6d1-a298-4dd5-acc6-d90ab840d97d@googlegroups.com>
In reply to#101489
On Wednesday, January 11, 2017 at 5:37:50 PM UTC-6, Richard Heathfield wrote:
> Yes, absolutely. The diagnostic message is a requirement on the 
> implementation, but it is not under any obligation to stop translation.

An implementation is required to produce some kind of diagnostic when
given any file containing a constraint violation, but for constraint
violations not involving the #error directive the Standard doesn't require
that the "diagnostic" have anything to do with the violation.  A conforming
implementation could output "This is a diagnostic" as its sole message when
given *any* input, well-formed or otherwise, which does not contain an
#error directive.

[toc] | [prev] | [next] | [standalone]


#101502

FromRichard Heathfield <rjh@cpax.org.uk>
Date2017-01-12 09:41 +0000
Message-ID<o57itu$f1m$1@dont-email.me>
In reply to#101497
On 12/01/17 05:49, supercat@casperkitty.com wrote:
> On Wednesday, January 11, 2017 at 5:37:50 PM UTC-6, Richard Heathfield wrote:
>> Yes, absolutely. The diagnostic message is a requirement on the
>> implementation, but it is not under any obligation to stop translation.
>
> An implementation is required to produce some kind of diagnostic when
> given any file containing a constraint violation, but for constraint
> violations not involving the #error directive the Standard doesn't require
> that the "diagnostic" have anything to do with the violation.  A conforming
> implementation could output "This is a diagnostic" as its sole message when
> given *any* input, well-formed or otherwise, which does not contain an
> #error directive.

The implementation doesn't even have to say that the diagnostic message 
is diagnostic. It could, for example, respond to syntax errors or 
constraint violations by printing "honi soit qui mal y pense". Or even 
"translation succeeded"!

-- 
Richard Heathfield
Email: rjh at cpax dot org dot uk
"Usenet is a strange place" - dmr 29 July 1999
Sig line 4 vacant - apply within

[toc] | [prev] | [next] | [standalone]


#101503

FromNoob <root@127.0.0.1>
Date2017-01-12 10:52 +0100
Message-ID<o57jie$h2u$1@dont-email.me>
In reply to#101489
On 12/01/2017 00:37, Richard Heathfield wrote:

> On 11/01/17 23:29, Noob wrote:
> 
>> I may have oversimplified my example. The actual situation
>> looks more like atexit. One registers a function that is
>> supposed to be called later, in specific circumstances.
>>
>> The actual code looks like this:
>>
>> struct foo { int i; };
>> extern struct foo *alloc_foo(void);
>> extern void api_func(struct foo *arg);
>> extern void register_func(void (*func)(void *), void *data);
>> /* register_func saves func and data, to be able to call func(data) later */
>> void user_code(void) {
>> 	struct foo *data = alloc_foo();
>> 	register_func((void (*)(void *))api_func, data);
>> }
>>
>>
>> A simple solution would be declaring a wrapper around api_func:
>>
>> void wrapper(void *arg) { api_func(arg); }
>>
>> and passing the wrapper to register_func.
>>
>> This is 100% portable, right?
> 
> FWIW, I can't see anything wrong with it. It's a shame you can't come up 
> with a consistent declaration for the registered functions, though, as 
> it would enable you to avoid an ugly cast.

I'm confused. There is no ugly cast in my "simple solution".
Which cast are you talking about?

void user_code(void) {
	struct foo *data = alloc_foo();
	register_func(wrapper, data);
}

Regards.

[toc] | [prev] | [next] | [standalone]


#101505

FromRichard Heathfield <rjh@cpax.org.uk>
Date2017-01-12 09:58 +0000
Message-ID<o57jt0$hj1$2@dont-email.me>
In reply to#101503
On 12/01/17 09:52, Noob wrote:
> On 12/01/2017 00:37, Richard Heathfield wrote:
>
>> On 11/01/17 23:29, Noob wrote:
<snip>
>>> 	register_func((void (*)(void *))api_func, data);
<snip>
>> [...] an ugly cast.
>
> I'm confused. There is no ugly cast in my "simple solution".
> Which cast are you talking about?

That one.

-- 
Richard Heathfield
Email: rjh at cpax dot org dot uk
"Usenet is a strange place" - dmr 29 July 1999
Sig line 4 vacant - apply within

[toc] | [prev] | [next] | [standalone]


#101514

FromNoob <root@127.0.0.1>
Date2017-01-12 13:08 +0100
Message-ID<o57rhu$ioj$1@dont-email.me>
In reply to#101454
On 11/01/2017 21:26, Noob wrote:

> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;

C99 6.3.2.3 p8
"A pointer to a function of one type may be converted to a pointer to a function of another
type and back again; the result shall compare equal to the original pointer. If a converted
pointer is used to call a function whose type is not compatible with the pointed-to type,
the behavior is undefined."

6.7.5.3 p15
For two function types to be compatible, both shall specify compatible return types.
Moreover, the parameter type lists, if both are present, shall agree in the number of
parameters and in use of the ellipsis terminator; corresponding parameters shall have
compatible types.

Are "void (*)(void *)" and "void (*)(struct s *)" compatible types?

It boils down to the function parameter:

Are "void *" and "struct s *" compatible types?

I think: "no", but I can't find definitive C&V... unless this?

6.2.7 p1 Compatible type and composite type
"Two types have compatible type if their types are the same. Additional rules for
determining whether two types are compatible are described in 6.7.2 for type specifiers,
in 6.7.3 for type qualifiers, and in 6.7.5 for declarators."

A type has a type? /me brain exploded

Regards.

[toc] | [prev] | [next] | [standalone]


#101519

FromBen Bacarisse <ben.usenet@bsb.me.uk>
Date2017-01-12 13:45 +0000
Message-ID<87pojsqtmx.fsf@bsb.me.uk>
In reply to#101514
Noob <root@127.0.0.1> writes:

> On 11/01/2017 21:26, Noob wrote:
<snip>
> Are "void (*)(void *)" and "void (*)(struct s *)" compatible types?
>
> It boils down to the function parameter:
>
> Are "void *" and "struct s *" compatible types?
>
> I think: "no", but I can't find definitive C&V... unless this?

Definitely no.  Type compatibility is very strict: char is not even
compatible with one of signed char and unsigned char.

> 6.2.7 p1 Compatible type and composite type

> "Two types have compatible type if their types are the
> same. Additional rules for determining whether two types are
> compatible are described in 6.7.2 for type specifiers, in 6.7.3 for
> type qualifiers, and in 6.7.5 for declarators."
>
> A type has a type? /me brain exploded

I think that's a typo that comes from incomplete editing.  Maybe it once
said something like "[t]wo values have compatible type if their types
are the same" and someone decided to focus on the types (since types
exists independently of values or objects) and failed to change "their"
to "the".

tl;dr: type compatibility is exact type equality with a few tweaks.

-- 
Ben.

[toc] | [prev] | [standalone]


Back to top | Article view | comp.lang.c


csiph-web