Skip to main content

skip to main content

developerWorks  >  Power Architecture technology  >

Testing and measuring the TAMS 3011, Part 3: Porting a screen-management utility to eCos

Curses, foiled again!

developerWorks
Document options

Document options requiring JavaScript are not displayed


Rate this page

Help us improve this content


Level: Introductory

Peter Seebach (developerworks@seebs.plethora.net), Freelance author, Plethora.net

24 Mar 2006

See the process of porting the Berkeley curses library from UNIX® to eCos, picking up a few fragments of the Berkeley C library extensions along the way -- and learn about some general issues of porting from UNIX to eCos.

In the brief window between the switch away from hardcopy terminals and the generally safe assumption that everyone was using a vt100 emulator (a mere twenty years or so), was a need for a standard and portable way for applications running on UNIX-like systems to interact with addressable terminals. In fact, this problem is deep enough that the solution has more than one layer. The curses library provides a standardized way to interact with addressable terminals, but curses itself is built on another library, which provides a database of terminal capabilities: the termcap database. In fact, some curses implementations now prefer the terminfo system, but the essential pattern is the same: curses provides a set of higher level functions, which are in turn based on a database of terminal features.

Many interesting applications are developed on top of curses; the next article in this series explores one of them. For now, the goal is just to get curses itself running on eCos, so that you can write simple test programs using it.

The most common and complete curses implementation, ncurses, is a complete (well, so far as I can tell, anyway) implementation of the entire SVR4 curses API, built to run on top of terminfo. It's beautiful, but it's also huge.

Since eCos is a small embedded system, I'm going to look at the much smaller and simpler Berkeley curses and termcap libraries. These in turn depend on a couple of extended features of Berkeley libc, but luckily, it turns out that you can easily extract these features from the library without dragging in the whole thing.

Getting the source

Porting an application library can involve a bit of iterating. For starters, I just grabbed *.c and *.h from the libcurses and libterm directories on a NetBSD box. The source came from a January 3rd, 2006 update of NetBSD -- not that the curses code is being changed a lot these days; while every file here has been touched this century, it's a close call for some of them.

The scripts supplied as part of the eCos distribution which build sample Makefiles for target applications assume an environment where all the source for the target program is in one directory. They don't really cover the situation where you wish to build the application and a support library independently and link them both with the operating system. So, rather than spend hours messing with them, I used them to build a Makefile, then modified it by hand; one version (used for the libraries) generates a library archive of the provided source, and the other version (used for the test executable) links against those libraries.

The original build tree had two directories: a kernel directory (for the eCos kernel build, made with the ecosconfig utility) and an application directory. I simply added three new directories: "curses," "term," and "include." The "include" directory allows you to consolidate in one place the various header files that the libraries use.

With this in mind, you can try to compile the curses library. The first problem encountered is that the __RCSID macro used in NetBSD source files isn't defined, leaving you with a very serious syntax error. This is simple enough to resolve, with the command-line flag -D'__RCSID(x)='. (The single quotes keep the shell from trying to interpret the parentheses; otherwise, this would be a shell syntax error, and the compiler wouldn't even get called.)

The next problem is that curses predates modern C's conventions for who defines the bool type and where; the easiest thing to do is simply remove the typedef from curses.h. The next problem is the use of the _BSD_VA_LIST_ macro, but you can simply replace this with va_list.

Finally, you run into references to u_int32_t; the obvious fix (use the ISO spelling uint32_t) won't work, because the headers default provides don't define that either! In fact, you'll find the needed definitions in the version of <machine/types.h> in the eCos kernel source, but not in the version the cross compiler uses. It's simpler, for now, to just change all references to u_int32_t (and uint_32t) to unsigned long, and all references to u_int to unsigned int.

Being lazy by nature (often an asset in a programmer), I didn't spend a lot of time figuring out how to get the various canonical integer type macros in scope. Given that they exist in the source somewhere, it's probably possible, but in this case, I know my target system reasonably well anyway. This might not be portable to a 64-bit system. The correct fix (add a proper inttypes header to eCos and send the changes back to the developers) is beyond the scope of this article.

This is enough modification that at least some files including curses.h can be compiled (not all of them, though). The next compilation problem is a little more subtle: the __warn_references macro produces in-line assembly. It's not that the macro isn't defined; it's that the definition doesn't work as desired. The easiest thing is to just remove all uses of it; it shows up in three files, four times in each. This, plus a few recurrences of previous issues (uses of u_int and _BSD_VA_LIST_) gets much of the code compiling.

Now, only three modules still won't compile: setterm.c, tstp.c, and tty.c. In all cases, the compilation problems reflect use of BSD extensions.



Back to top


Terminal settings and ioctl

The problem with the setterm.c code has to do with trying to use the TIOCGWINSZ ioctl to query the size of a window. Well, there isn't any such ioctl, so you can simply remove this code, and just check the termcap entry or environment (thanks, POSIX!). You can also safely remove the references to <sys/ioctl.h>. They're only there to try to pick up the definition of TIOCGWINSZ on a few old systems. The code in tstp.c is similar, although much more elaborate. It not only depends on TIOCGWINSZ, but on SIGWINCH and SIGTSTP, neither of which exist on eCos. These are easy to find -- just look at the line numbers in the compiler output.

You also see here the first references to TCSASOFT, an ioctl flag which eCos doesn't have. Removing the first two is fairly trivial. The logic for using TCSASOFT is interesting; in fact, it's used only if a variable (__tcaction) has been set -- and it is set if and only if TCSASOFT is #defined. Unfortunately, the macro still isn't defined, so this doesn't do any good. Just replace it with 0.

This gets you down to one compilation error: tty.c refers to the fpurge() function, which is a BSD extension allowing the flushing of input streams. You can't do that, so you have to hope that the driver doesn't buffer too much stuff you don't want. There's no portable way to flush input buffers in C. In practice, it doesn't affect most curses applications, because the majority of them run in raw mode anyway.

And that does it! The system can now build libcurses.a. Of course, an application linked with it won't get very far, but we've made some progress.



Back to top


Termcap entries

The termcap library is, of course, not ready to compile. Copying the Makefile over from the curses library and replacing the "SRCS=..." line with a list of the termcap source files gets the compile as far as the first file, which errors out horribly.

However, this time, you have a better option than searching the headers for a matching definition. What's missing is MAXPATHLEN. You use this for looking at files...but you don't want to look at files. Files would rely on a filesystem, and you don't want a filesystem on a tiny embedded system if you don't really need it. So instead, you want to remove the code that depends on that declaration.

Here's where the Berkeley termcap library pays its way. The default behavior of termcap is that, if the environment variable TERMCAP is set to contain a termcap entry, that will be used. So, go with that by taking the standard vt100 termcap entry and simply embedding it in the file as a string:


Listing 1. The vt100 termcap entry, as C source

	static const char *vt100 =
	"vt100|vt100-am|dec vt100 (w/advanced video):"
	[...]
	":up=\E[A:us=\E[4m:";

Note that each line of this declaration is a quoted string; ISO C concatenates quoted strings automatically, so no special formatting is needed. Now, just replace getenv("TERMCAP") with strdup(vt100), and the code is set up to work on a copy of the entry.

The next chunk of code to work on is the fairly torturous and elaborate code used to try to find terminal entries, possibly expanding tc attributes, searching paths, and so on. It all seems to come down to this:


Listing 2. Encapsulating nearly 150 lines of code

	(*bp)->info = strdup(vt100);
	i = 1;

This leaves you with a few missing functions. You can find one of them, strlcpy, from many sources. I grabbed it from XFree86 (see Resources). You can get the cgetnum and related functions from NetBSD's libc, where they're in getcap.c. Finally, you'll notice the usage of asprintf. That usage corresponds to part of the weird magic termcap uses to make multiple references more efficient. You can remove it.

Trying to compile getcap.c seems horribly daunting, but the hard parts are predominantly in the database code. Database code?, you might ask. Well, there's support for using Berkeley DB files to look up entries. This is useless bloat, and is omitted on small systems; just add #define SMALL to the file and watch the problems vanish. You do still need declarations for all these functions. On NetBSD, they were in <stdlib.h>. Adding them to a new header, "getcap.h," is easy enough, and only termcap.c and getcap.c need to include it. Only termcap.c needs a declaration for strlcpy().

You'll notice a few more uses of u_int (just replace with unsigned int) and some references to E2BIG (ERANGE is close enough).

Another bullet dodged is the reference to fgetln() in termcap.c, which looks important until you realize that "next entry in the database" never applies to your file-free implementation; since you never call cgetfirst() or cgetnext(), you can remove the entire functions.



Back to top


So, about that application

Here's our newly revised sample application:


Listing 3. A trivial curses program

	#include <stdarg.h>
	#include <curses.h>

	int
	main(void) {
		initscr();
		printw("hello, world!\n");
		refresh();
		endwin();
		return 0;
	}

Trying to compile this and link it with both libraries produces a surprisingly small set of clashes. The first, and simplest to resolve, is that both the eCos libraries and the curses/termcap library are using the name PC. I renamed the version in curses/termcap __tc_PC, for consistency with the rest of the curses interface.

The hard one is funopen(), a BSD extension allowing the creation of a new file stream using a provided function; this is used to execute an fprintf that writes directly to the window.

For the fairly common case, where a single print operation writes less than 2K of text, replace the following code:


Listing 4. The funopen() function in use

        if ((f = funopen(win, NULL, __winwrite, NULL, NULL)) == NULL)
                return (ERR);
        (void) vfprintf(f, fmt, ap);
        return (fclose(f) ? ERR : OK);
 

with this:


Listing 5. Using vsnprintf() to write text

        char buf[2048];
        int n = vsnprintf(buf, 2048, fmt, ap);
        return (__winwrite(win, buf, n) == n) ? OK : ERR;
 

With this, the program compiles. It crashes, of course, when loaded on the actual hardware. It turns out that the correct value for i in t_getent was 0, not 1; setting it to 1 caused the curses code to believe (mistakenly) that the terminal wasn't identified correctly.



Back to top


Picking your drivers

The next test is a slightly more complicated program, which just echoes back the characters it receives:


Listing 6. A very slightly less trivial curses program

	#include <stdarg.h>
	#include "curses.h"

	int
	main(void) {
		int c;
		initscr();
		printw("hello, world!\n");
		cbreak();
		noecho();
		refresh();
		while ((c = getch()) != EOF) {
			printw("%c[%#x]", c, c);
		}
		endwin();
		return 0;
	}

This program didn't work for me on the default system. The default serial drivers are the fairly naive diagnostic serial drivers, rather than the interrupt-driven ones which handle raw mode input correctly. The behavior I saw, which was very odd, was that every other character sent was simply lost. The characters received were echoed immediately, and line editing worked, and when you sent a carriage return, the whole line was handed to the curses application at once. With the real serial drivers, everything worked as hoped.

Configuring the serial drivers was pretty easy. A friendly developer on the eCos mailing list sent me a file to import (with the ecosconfig utility) which enabled the better serial drivers:


Listing 7. CDL code to enable the interrupt-based serial drivers

	cdl_option CYGDAT_IO_SERIAL_TTY_CONSOLE {
	    user_value "\"/dev/termios0\""
	};

	cdl_component CYGPKG_IO_SERIAL_TERMIOS_TERMIOS0 {
	    user_value 1
	};

	cdl_component CYGPKG_IO_SERIAL_DEVICES {
	    user_value 1
	};

	cdl_option CYGNUM_IO_SERIAL_POWERPC_PPC405_SERIAL0_BAUD {
	    user_value 38400
	};
 

And that's it. Curses running smoothly on a system with no filesystem support. To the best of my knowledge, the only bug introduced is that individual wprintw operations exceeding 2047 characters will be truncated; given that most applications do their own line wrapping management, this is probably irrelevant.



Back to top


Lessons learned

A recurring theme in trying to build a stripped-down version of a library is that the parts which are hardest to port are the parts you can most likely live without. The hardest part of the termcap code is the search path mechanism, which happens to be irrelevant in this case. The functions that would be hardest to duplicate, such as fgetln(), are most likely to be used in the middle of whole chunks of code you don't need in your embedded environment anyway.

Porting has become a lot easier as better standardization has given more reliable APIs; there was a time when the idea of porting 12,000 lines of library code from a multiprocessing server OS to a single-process embedded platform in an idle evening would have received only hoots of laughter.

It's easy to imagine dismissing eCos because, for instance, "there's no curses support," but in fact, getting curses working on eCos took only a few hours, certainly solidly under a day's work. The basic support for portable applications is there, and the executable size is unbeatable; the whole application is 200KB, including kernel, drivers, curses, and termcap.



Resources

Learn

Get products and technologies

Discuss


About the author

Peter Seebach

Peter Seebach first had to debug a curses implementation in 1989. He's gotten used to it.




Rate this page


Please take a moment to complete this form to help us better serve you.



YesNoDon't know
 


 


12345
Not
useful
Extremely
useful
 


Back to top


UNIX is a registered trademark of The Open Group in the United States and other countries. Other company, product, or service names may be trademarks or service marks of others.