Sunday, January 31, 2021

Personalizing Hello World -- A Greet Command

[TOC

Continuing with another version of Hello World! to extend our beachhead, let's say we want the Hello! program to be less general. Specifically, instead of having the computer greet the world, let's write a program that allows the user to tell the computer whom to greet: 


/* A greet command beachhead program
** as a light introduction to command-line parameters.
** This instance the work of Joel Rees,
** Whatever is innovative is copyright 2021, Joel Matthew Rees.
** Permission granted to modify, compile, and run
** for personal and educational uses.
*/


#include <stdio.h>
#include <stdlib.h>


int main( int argument_count, char *argument_variables[] )
{
  char * whom = "World";

  if ( argument_count > 1 )
  {
    whom = argument_variables[ 1 ];  /* Where did the initial value go? */
  }
  fputs( "Hello ", stdout ); /* Still avoiding printf(). */
  fputs( whom, stdout );
  putchar( '!' );
  putchar( '\n' );
  return EXIT_SUCCESS;
}

Comparing this to the first exercise, we see that we are actually using those command-line parameters. I'd like to have postponed that a bit further because they are a rather confusing beast. And some people who want to follow along want to do so on platforms that don't have command-line parameters under the usual operating system interface. (Such as the original/Classic Mac without MPW, and ROM-based game machine/PCs like the Tandy/TRS-80 Color Computer without Microware's OS-9, etc.) 

But I have reasons. 

For now, just kind-of assume that there is more to them than meets the eye. 

(If your platform won't allow you to follow along, read the explanation, examine the screenshot carefully, and at least consider downloading Cygwin or installing a libre *nix OS so you can actually try these. For these purposes, an old machine sleeping somewhere might work well with NetBSD or a lightweight Linux OS.)

Again, if you are using a K&R (pre-ANSI) compiler like Microware's compiler for OS-9, move the function parameters for main down below the declaration line. Also, shorten the parameter names, since those compilers typically get confused over long names that start too much the same -- which is the real reason argument_count is usually written argc and argument_variables is usually written argv:

int main( argc, argv )
int argc;
char *argv[];
{
/* etc. */
}

And I'm throwing another fastball at you. There is a conditional in this program. Conditionals are another thing you should assume I'm not telling the whole story about here.

But be aware that, while puts(), fputs(), and putchar() are function calls, 

if ( condition )  {  }

is not. Nor is it a function declaration, such as   

void my_puts( char * string )
{
  ...
}

which you might recall from the first exercise.  

It's a test of a condition. If the condition between the parentheses evaluates to true, the stuff between the braces gets done. If not, the stuff between the braces gets jumped over. (The braces aren't required if there is only one statement to be jumped over, but they are advised for a number of reasons. And there is an optional else clause. And the values of true and false need to be discussed. More detail later.)

Note also that arrays are indexed from 0 up to the array size minus 1. Thus, the first element of an array is element array[ 0 ]. And the last is array[ ARRAY_SIZE - 1 ], for a total of ARRAY_SIZE elements.

If you were compiling to M6809 object code and had the compiler output the assembler source, you would see something like the following -- except that I have added explanation. 

(I'm not asking you to learn 6809 assembly language, just giving it as something to hang my comments on.)

I've mixed in the original C source on the comment lines that start with an asterisk. On code lines, everything following a semicolon is my explanatory comments:


* int main( int argument_count, char *argument_variables[] )
s0000 FCC "World"  ; Allocate the string.
 FCB 0             ; NUL terminate it.
s0001 FCC "Hello " ; See above.
 FCB 0

_C_main
* {
*   char * whom = "World";
 LEAU -2,U   ; Allocate the variable whom.
 LDX #s0000  ; Load a pointer to the World string and
 STX ,U      ; store it in whom.
*
*   if ( argument_count > 1 )
 LDD 2,U     ; Get argument_count.
 CMPD #1     ; Compare it to 1.
 BLE _C_main_001  ; If less than or equal to 1, branch to _C_main_001
*   {
*     whom = argument_variables[ 1 ];  /* Where did the initial value go? */
             ; This code is executed if argument_count is 2 or more.
 LDY 4,U     ; Get the pointer to the argument_variables array.
 LDX 2,Y     ; Get the second pointer in the argument_variables array.
 STX ,U      ; Store it in whom.
*   }
_C_main_001
*   fputs( "Hello ", stdout ); /* Still avoiding printf(). */
 LDX #_f_stdout ; Get the file pointer and
 PSHU X      ; save it as a parameter.
 LDX #s0001  ; Get a pointer to the Hello string and
 PSHU X      ; save it as a parameter.
 JSR _fputs  ; Call (jump to subroutine) fputs() --
             ; fputs() cleans up U before returning.
*   fputs( whom, stdout );
 LDX #_f_stdout ; See above.
 PSHU X
 LEAX ,U     ; Get the address of whom and
 PSHU X      ; Save it as a parameter.
 JSR _fputs
*   putchar( '!' );
 LDD #'!'
 PSHU D
 JSR _fputchar  ; putchar also cleans up the stack after itself.
*   putchar( '\n' );
 LDD #_c_newline
 PSHU D
 JSR _fputchar
*   return EXIT_SUCCESS;
 LDD #_v_EXIT_SUCCESS  ; Leave the return value in D.
 LEAU 2,U  ; Clean up the stack before returning.
 RTS  ; And return.
* }

(Unless I say otherwise, all my assembly language examples are hand-compiled and untested. But I'm fairly confident this one will work, with appropriately defined libraries using a properly split stack.)

(If you understand stack frames, note that this code uses a split stack and does not need explicit stack frames. The return PC is on the S stack, out of the way. Thus the parameters are immediately above the local variables.) 

Again, don't worry how well you understood all of that.

Just note the code produced for the if clause produces code that tests argument_count, and if it is 1 or less skips the following block. If it is 2 (or more) the following block is executed, and the char pointer whom gets overwritten by the second command-line parameter.

Don't assume you know all there is to know about conditionals from that short introduction, any more than you know all about the command-line parameters. Compile it and run it and maybe add some code to get a look at the first entry in argument_variables[] if you're interested and can immediately see how. That's good for now.

I guess we'll get a screen shot of compiling and running this.

Details:

rm greet

deletes a previously compiled version of the program.

ls

as before, lists the files in the current directory. I've saved this version in greet.c, so

cc -Wall -o greet greet.c

will compile the program, with full syntax checking. 

./greet

calls it without parameters. (Except for that first one we haven't looked at yet.)

./greet Harry

calls it with one. (Ergo, two.)

./greet Harry Truman

calls it with two (ergo, three). How would you get a look at the second/third one?

You might be interested to see what is in the first actual command-line parameter. Or you might not be interested. I've mentioned that you could get at it. Can you think of a way to do so? If you do, do you recognize what it contains? 

(The first argument actually varies from platform to platform, but it isn't something the user usually consciously specifies, which is why it isn't usually counted as a parameter. I won't spoil the surprise here, but I will explain later.)

And you might also be interested in looking at the assembly language output for the processor you are using. The command-line option for that on gcc is the -S option, which looks like this: 

cc -S greet.c

You can use the -Wall options as well, like this:

cc -S -Wall greet.c

Either way, that will leave the assembly language source output in a file called greet.s , and you can look at it by bringing it into your favorite text editor, or with the more command, etc.

Where does the string that whom gets initialized with go, by the way? 

Nowhere. But we didn't save the pointer to it anywhere, so it just becomes (more-or-less) inaccessible. It's short enough we don't care too much, especially in this program, but it's just cluttering up memory. 

There's a lot to think about here, so let's keep it short. The next one one is going to be pretty long, when I get it put together and really deep.

Before the next one is up, or before you go look at it, play with this one a bit more. Again, explore. See what happens if (whatever gets your curiosity up), and then see if you can find a reason why.

 And the next one is ready now, here. We'll give the user a menu to choose whom to greet.

[TOC]

No comments:

Post a Comment