 | 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.
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.
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.
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.
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.
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 first had to debug a curses implementation in 1989. He's gotten used to it. |
Rate this page
|  |