11/07/2011

Make Your Own Hacker Typer 2

Last time we saw how to make the base for our version of Hacker Typer. Now, we could add some additional features, so we could choose which file we want to "type" and how many characters should output when we press a key.

Now, these are useful things to do, because we will include some more functions, and work with strings for the first time. Strings are nothing else then arrays of chars, terminated by zero as the mark for the end of string. So, the first thing we will do is requesting input for how many chars to type per key press, and then what file to output once we start typing. After that, we open the file, and pass the file pointer and the number of chars per key to the function.

We will change function so that it has a for loop which will fill up the buffer with chars read from the file. After every completed loop, we will output the contents of the buffer using a new function, fputs(), instead of putchar(). Why fputs(), why not just puts()? Well, fputs() uses the output stream (i.e. file pointer) as the second argument, and doesn't terminate the string with the newline character, which puts() regularly does. We don't want groups of three characters in separate lines, do we? Try and see how it works (puts() takes only one argument - pointer to the beginning of the string).

Also, we should use something called dynamic memory allocation, so we could make space for the buffer exactly chnum (new argument) characters long. That's because we don't actually know what number could the user enter in the beginning. We could limit that number with lower bound 1 and upper bound, let's say, 8 (8 chars per key press is pretty awesome speed, don't you think?), but it could theoretically be 250 or 500... And we should learn something new and really useful today ;)

So, we're going to use malloc() function from stdlib.h (we should include it at pre-processing). This function takes the size of memory needed (in bytes) and returns a void pointer to the beginning of the allocated memory block. void *? But we need char *! Never mind, we will just cast it. Cast? What the heck?

Assume you have a memory location and something written in it. You have many ways to interpret its contents. You can interpret it as char, int, long, float, double, address, unsigned, struct, whatever. Computer uses bytes to store data, and bytes can be read in different ways, depending on the convention. So, now we have an address, with a void pointer assigned to it. Never mind, just tell the compiler to interpret it as an address with a char pointer. Pointers are basically just addresses, so if you change their type, you won't change their content. (This is pretty weak explanation, and requires a separate tutorial post just for memory, addressing and pointers; for now, take it as this, if you already don't know).

Wait, so how many bytes should we allocate? Use sizeof() operator for this. It returns the size of a variable, constant, even data type in bytes. And, how many chars we need? One for the terminating zero, and chnum for the string, so chnum+1. We will multiply it by the size of char in bytes.

When you assign a block of continual memory to a pointer, that pointer starts to behave like an array. Actually, array is nothing else than pointer to the beginning of a continual memory block for storing some type of data. Addressing a specific element in array is done by putting the number in order (a.k.a. index) between brackets. Like, buf[2] will be the 3. element in the buf array (Remember: indexes in C start from 0).

So, when we fill up the buffer, we just put it as a string to stdout. stdout is pointer to the standard output, usually console window. Notice stdout behaves like a file open for writing. Additionally, stdin is standard input (keyboard) and behaves like a file open for reading. There exists stderr, standard error output (usually console window or a printer), and it's used as output for error messages.

Phew! Finally, here's the modified function code:
int hackertyper(FILE *fin, int chnum)
/*  new argument:
     int chnum - number of characters to type
     on one keypress
*/
{
  char key;
  char *buf = (char *)malloc(sizeof(char)*(chnum+1));
  int i;
  /* key - key read from the keyboard
  buf[] - buffer for chars read from the file
  i - counter */

  buf[chnum]=0;                // ensure end of string
  while ((key=getch())!=27)    // ascii for ESC key
  {
    for (i=0; i<chnum; i++) {  // fill buffer
      if (feof(fin)) rewind(fin);
      buf[i]=getc(fin);
    }
    fputs(buf, stdout);        // output buffer
  }
  return 0;  // zero exit code - no error
}
In the main file, we will add a new variable for choosing number of characters per key press, and a new variable for the desired file name. This time, we will assume file name is not more than 254 chars long, so will make an array of 255 chars. We will use scanf() with %s parameter for reading the file name, because we need to ensure there are no spaces in the file name and that we read exactly one string. White spaces are ' ', '\n', '\t'.

The main function should look something like this:
int main()
{
  int noc;        // number of chars to type
  char nof[255];  // name of the input file
  FILE *dat;      // file pointer
  printf("Number of characters per keypress: ");
  scanf("%d", &noc);
  if (noc<=0) noc=1;
  printf("File to type (/ for default): ");
  fflush(stdin);  // flush the rest after the first scanf
  scanf("%s",nof);
  if (!strcmp(nof, "/"))     // if default
    strcpy(nof, "hcktp2.c"); // nof="hcktp2.c"
  dat = fopen(nof, "r");
  if (dat==NULL) { // if file doesn't exist
    printf("Error reading file %s, make sure the file exists.\n", nof);
    return 1;
  }
  hackertyper(dat, noc);
  return 0;
}
Now, see the modifications. We check if number of characters per key press is less than or equal to zero; if it is, we will set it to 1, that's our lower bound. You can set the upper bound the same way, just turn over the sign.

Next, we fflush(stdin), because we need to ensure we will not read additional characters after the number entered as a part of the filename. Next, we read the filename. We should now check if the user wants default file to be loaded. We will use strcmp() function from string.h (don't forget to include it), which takes two strings and compares them alphabetically. It returns -1 if the first is alphabetically before the second, 0 if they are equal, or 1 otherwise. So, if the entered string is equal to "/", we will assume the user wants default file to be loaded. In that case, we should copy the default name to our filename variable, using function strcpy(). Why can't we just use =? Because it doesn't work that way; remember filename is an array, array is a pointer, and writing something like nof=whatever would try to change the address written in nof. If the right value cannot be interpreted as an address, you will get error message. So, strcpy() copies the contents of the second argument over the contents of the first, until terminator char (0) is found in the second argument.

Now, the rest is pretty much intuitive. Note we try to open a user-entered file, so we should first check if the file exists. We will just check if it's open for reading; fopen() will return NULL if it's not, and that's the sign we should print error message and terminate the program.

Some more checks could be added, but I will leave that to you for experimenting. The best way to learn programming is experimenting, and finding mistakes in your own code. Test it, make a mistake, try to do unexpected things, and see what you get. Just ensure you're in protected regime (which you probably are, if you're working on some modern multitasking operating system), so you can't mess up something important (which is almost impossible). And watch out what you're doing with memory; use only the memory you reserved by your program, otherwise you could possibly rewrite some other process memory and cause a temporary crash.

You can download the full source: hcktp2.c

As last time, you should check if you're in Windows or Linux before compiling, because of getch() function. If you have any troubles, find any errors, have some suggestions or you want to know more, please post it down in the comment section, and we can discuss it.

2 comments:

  1. Hey, really nice example. I actually learned a few things here. However, there are several things I dont like in your code, but the thing that bothers me the most is the misuse of fflush(). You kind of explained what you wanted to accomplish, but this simply does not work.

    ReplyDelete
  2. God, I wish I were here all this time, now I am afraid it's too late to respond :( Yes, you're actually right, this doesn't work on every compiler. I searched a little bit and found out that it's undefined behavior in ANSI C, so it may or may not work. On some compilers, however, it should clear the standard input buffer, but probably a better solution would be to write "while(getchar()!='\n');" instead. I/O business can be really tricky in C.

    Thank you for your comment, and please tell me what else you don't like about it. And now (after half a year) I see I used scanf() for strings, whereas it would be better to use fgets(). Or maybe to specify string length in format string, like "%254s". Well, looks like I am still a noob indeed, after 6 years.

    ReplyDelete

If you have anything useful to say (ideas, suggestions, questions, business proposals, or just "Thank you!"), please feel free to comment! Also, you can use my e-mail and/or find me on Twitter!