jonm.dev

The Art of Writing Software



VerySillyMUD: Cleaning up the Warnings

Series [ VerySillyMUD ]

This post is part of my “VerySillyMUD” series, chronicling an attempt to refactor an old MUD into working condition [ 1 ].

In our last episode, we discovered a bug that the compiler had actually warned us about, but which we ignored because we were trying to just fix errors and get on with running the game. Lesson learned here; time to go clean up all those warnings. As there are a lot of them, and they are likely going to be somewhat repetitive, I will just highlight the particularly interesting ones here. However, you can see the full set of cleanups here.

The first thing we want to do is to recompile all the object files to see what the warnings are. I was about to make clean except that the Makefile doesn’t seem to have a clean target! Let’s fix that. Then, let’s make sure we don’t make this mistake again by adding the -Werror flag to the CFLAGS variable to treat compiler warnings as errors. Now, the first type of error we encounter is:

$ make
gcc -g -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -Werror -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -c -o comm.o comm.c
comm.c:163:25: error: implicit declaration of function 'atoi' is invalid in C99
[-Werror,-Wimplicit-function-declaration]
} else if ((port = atoi(argv[pos])) <= 1024) {
^
...
20 errors generated.
make: *** [comm.o] Error 1

Basically, we have not explicitly declared a prototype for the atoi library function, nor have we #included one. This is simply a matter of adding the right system include file in the case of C library functions. Next:

[~/src/SillyMUD/src]$ make
gcc -g -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -Werror -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -c -o comm.o comm.c
comm.c:164:8: error: add explicit braces to avoid dangling else
[-Werror,-Wdangling-else]
} else if ((port = atoi(argv[pos])) <= 1024) {
^
...
fatal error: too many errors emitted, stopping now [-ferror-limit=]
20 errors generated.
make: *** [comm.o] Error 1

When you are not explicit about your braces, and you have nested if statements, you can have unintentional paths through your code when an else gets associated with the wrong if. We can correct these by adding explicit braces, taking indentation in the source code as a hint as to what was probably intended. Next we find that adding in some missing #include files created some conflicts:

[~/src/SillyMUD/src]$ make
gcc -g -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -Werror -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -c -o comm.o comm.c
comm.c:61:5: error: redefinition of 'reboot' as different kind of symbol
int reboot = 0; /* reboot the game after a shutdown */
^
/usr/include/unistd.h:680:6: note: previous definition is here
int reboot(int);
^
...
11 errors generated.
make: *** [comm.o] Error 1

A global variable reboot is used to track whether the game is supposed to restart itself after an intentional, non-crash shutdown. This conflicts with the reboot system call that actually reboots the whole machine! We can handle this by renaming the variable, taking care to find all locations of it in other files.

We also fixed some cases where functions were declared with a non-void return type yet there was no explicit return statement. As we saw in a previous episode, we can fix these by changing the declaration to return void if no callers ever check for a return value. Our next problem involves the type of one of the arguments passed to the bind() system call:

[~/src/SillyMUD/src]$ make
gcc -g -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -Werror -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -c -o comm.o comm.c
comm.c:755:14: error: incompatible pointer types passing 'struct sockaddr_in *'
to parameter of type 'const struct sockaddr *'
[-Werror,-Wincompatible-pointer-types]
if (bind(s, &sa, sizeof(sa)) < 0) {
^~~
/usr/include/sys/socket.h:557:38: note: passing argument to parameter here
int bind(int, const struct sockaddr *, socklen_t) __DARWIN_ALIAS(bind);
^
...
9 errors generated.
make: *** [comm.o] Error 1

A glance at the code shows that sa is declared as a struct sockaddr_in (an Internet socket address), but bind wants a pointer to a struct sockaddr. Here’s one of those cases where C expects you to cast one struct to another with the same prefix, for as much confusion as that might cause. This is a standard C TCP/IP socket programming idiom, however. The next error complains of an implicit function definition, but is a little more involved:

[~/src/SillyMUD/src]$ make
gcc -g -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -Werror -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -c -o comm.o comm.c
comm.c:1514:18: error: implicit declaration of function 'CAN_SEE_OBJ' is invalid
in C99 [-Werror,-Wimplicit-function-declaration]
case 'o': i = OBJN(obj, to);
^
./utils.h:239:26: note: expanded from macro 'OBJN'
#define OBJN(obj, vict) (CAN_SEE_OBJ((vict), (obj)) ? \
^
...
3 errors generated.
make: *** [comm.o] Error 1
view raw can_see_obj.txt hosted with ❤ by GitHub

With a little judicious grepping, we find that CAN_SEE_OBJ is declared in utility.c, where we see:

int CAN_SEE_OBJ( struct char_data *ch, struct obj_data *obj)
{
if (IS_IMMORTAL(ch))
return(1);
/* ... */
return(1);
#if 0
#define CAN_SEE_OBJ(sub, obj) \
( ( (!IS_NPC(sub)) && (GetMaxLevel(sub)>LOW_IMMORTAL)) || \
( (( !IS_SET((obj)->obj_flags.extra_flags, ITEM_INVISIBLE) || \
IS_AFFECTED((sub),AFF_DETECT_INVISIBLE) ) && \
!IS_AFFECTED((sub),AFF_BLIND)) && \
(IS_LIGHT(sub->in_room))))
#endif
}
/* This is an excerpt from a DikuMUD-derived codebase. DikuMUD was created by Sebastian Hammer, Michael Seifert</a>,
Hans Henrik Stærfeldt, Tom Madsen, and Katja Nyboe. This code is subject to the DikuMud License, as found at
https://github.com/jonm/SillyMUD/blob/43344e6dc864de7518c2fc0dbf7b7cf14f5924a2/doc/license.doc
*/
view raw can_see_obj.c hosted with ❤ by GitHub

Ugh. This apparently was a macro at one point but was redefined as a function, although someone conveniently left the old macro definition in here for us, but compiled out with #if 0. At any rate, we can clean that out and then add the missing prototype. For the next error, the compiler helpfully gives us options about how to fix it:

[~/src/SillyMUD/src]$ make
gcc -g -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -Werror -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -c -o comm.o comm.c
comm.c:1538:18: error: using the result of an assignment as a condition without
parentheses [-Werror,-Wparentheses]
while (*point = *(i++))
~~~~~~~^~~~~~~~
comm.c:1538:18: note: place parentheses around the assignment to silence this
warning
while (*point = *(i++))
^
( )
comm.c:1538:18: note: use '==' to turn this assignment into an equality
comparison
while (*point = *(i++))
^
==
...
2 errors generated.
make: *** [comm.o] Error 1

We can fix this by explicitly comparing the value of the assignment to a null character ('\0') instead. I also encountered some type errors showing up mismatches between a format string and an argument:

[~/src/SillyMUD/src]$ make
gcc -g -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -Werror -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -c -o act.info.o act.info.c
act.info.c:2785:58: error: format specifies type 'int' but the argument has type
'long' [-Werror,-Wformat]
sprintf(buf, "Total number of rooms in world: %d\n\r", room_count);
~~ ^~~~~~~~~~
%ld
/usr/include/secure/_stdio.h:47:56: note: expanded from macro 'sprintf'
__builtin___sprintf_chk (str, 0, __darwin_obsz(str), __VA_ARGS__)
^
act.info.c:2800:60: error: format specifies type 'int' but the argument has type
'long' [-Werror,-Wformat]
sprintf(buf, "Total number of monsters in game: %d\n\r", mob_count);
~~ ^~~~~~~~~
%ld
/usr/include/secure/_stdio.h:47:56: note: expanded from macro 'sprintf'
__builtin___sprintf_chk (str, 0, __darwin_obsz(str), __VA_ARGS__)
^
act.info.c:2803:59: error: format specifies type 'int' but the argument has type
'long' [-Werror,-Wformat]
sprintf(buf, "Total number of objects in game: %d\n\r", obj_count);
~~ ^~~~~~~~~
%ld
/usr/include/secure/_stdio.h:47:56: note: expanded from macro 'sprintf'
__builtin___sprintf_chk (str, 0, __darwin_obsz(str), __VA_ARGS__)
^
...
5 errors generated.
make: *** [act.info.o] Error 1

Again, the compiler happily provided a useful suggestion in these cases to update the format string. I think this is an area where the C compilers have improved since I was doing a lot of C programming; I’m not sure they would always catch these for me in the past. But I might also be misremembering!

Next, we have a couple spots where the developers wanted to be extra sure about order of operations:

[~/src/SillyMUD/src]$ make
gcc -g -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -Werror -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -c -o act.obj1.o act.obj1.c
act.obj1.c:51:39: error: equality comparison with extraneous parentheses
[-Werror,-Wparentheses-equality]
if((obj_object->obj_flags.type_flag == ITEM_MONEY)) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~
act.obj1.c:51:39: note: remove extraneous parentheses around the comparison to
silence this warning
if((obj_object->obj_flags.type_flag == ITEM_MONEY)) {
~ ^ ~
act.obj1.c:51:39: note: use '=' to turn this equality comparison into an
assignment
if((obj_object->obj_flags.type_flag == ITEM_MONEY)) {
^~
=
...
14 errors generated.
make: *** [act.obj1.o] Error 1

In this case, a quick perusal of the code suggests that this was indeed meant to be a comparison and not an assignment, so we can just get rid of the extra parens.

Now, at one point I ran into some type errors surrounding function pointers:

[~/src/SillyMUD/src]$ make
gcc -g -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -Werror -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -c -o act.off.o act.off.c
act.off.c:1238:3: error: incompatible pointer types initializing 'void (*)()'
with an expression of type 'void (byte, struct char_data *, char *, int,
struct char_data *, struct obj_data *)'
[-Werror,-Wincompatible-pointer-types]
cast_geyser,
^~~~~~~~~~~
...
6 errors generated.
make: *** [act.off.o] Error 1

Ugh, function pointers. I can never get the typing right on these and always have to look up how to do it. In this case, it looks like the programmers just got lazy, calling all the function pointers void (*)(), but we can propagate the correct types through the code instead.

And then I ran into this error:

[~/src/SillyMUD/src]$ make
gcc -g -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -Werror -DIMPL_SECURITY -DNEW_RENT -DLEVEL_LOSS -DNEWEXP -DGROUP_NAMES -c -o act.wizard.o act.wizard.c
act.wizard.c:243:24: error: format string is not a string literal
(potentially insecure) [-Werror,-Wformat-security]
sprintf(string, tmp);
^~~
/usr/include/secure/_stdio.h:47:56: note: expanded from macro 'sprintf'
__builtin___sprintf_chk (str, 0, __darwin_obsz(str), __VA_ARGS__)
^
...
7 errors generated.
make: *** [act.wizard.o] Error 1

Hmm, taking a look at the code shows:

/* Bamfin and bamfout - courtesy of DM from Epic */
void dsearch(char *string, char *tmp)
{
char *c, buf[255], buf2[255], buf3[255];
int i, j;
i = 0;
while(i == 0) {
if(strchr(string, '~')==NULL) {
i = 1;
strcpy(tmp, string);
} else {
c = strchr(string, '~');
j = c-string;
switch(string[j+1]) {
case 'N': strcpy(buf2, "$n"); break;
case 'H': strcpy(buf2, "$s"); break;
default: strcpy(buf2, ""); break;
}
strcpy(buf, string);
buf[j] = '\0';
strcpy(buf3, (string+j+2));
sprintf(tmp, "%s%s%s" ,buf, buf2, buf3);
sprintf(string, tmp);
}
}
}
/* This is an excerpt from a DikuMUD-derived codebase. DikuMUD was created by Sebastian Hammer, Michael Seifert</a>,
Hans Henrik Stærfeldt, Tom Madsen, and Katja Nyboe. This code is subject to the DikuMud License, as found at
https://github.com/jonm/SillyMUD/blob/43344e6dc864de7518c2fc0dbf7b7cf14f5924a2/doc/license.doc
*/
view raw dsearch.c hosted with ❤ by GitHub

Ok, I can see the problem, which is essentially some kind of nested sprintf calling. This needs to be refactored, but there’s no way this function is clear enough to me to refactor it without unit tests. Guess we’ll have to get that going in the next post.


[ 1 ] SillyMUD was a derivative of DikuMUD, which was originally created by Sebastian Hammer, Michael Seifert, Hans Henrik Stærfeldt, Tom Madsen, and Katja Nyboe.