The un*x and OS/2-EMX Porting Companion FAQ

(sorry for the long title ...)

Index

  1. Preface
  2. Copyright, License, Legal Stuff
  3. News
  4. Prerequisites
  5. Useful & Dirty Workarounds
  6. APIs that need Special Care
  7. Ported APIs
  8. un*x Process Stuff
  9. un*x Filesystem Stuff
  10. Misc Issues
  11. In & Out
  12. EMX Specifics
  13. Configuration Stuff
  14. Tools
  15. X11 Specifics
  16. Debugging
  17. Online Resources

Preface

This is a collection of tips & tricks for porting software from un*x (here "un*x" means "Unix- and Unix-like systems") to IBM's OS/2. If you need a more thorough introduction into the business of porting, you should check out the draft for the UNIX/POSIX to emx-OS/2 Porting Guide .
In general porting requires a broad range of knowledge, so this document has been extended to include general information about programming techniques, un*x, X11, etc. I assume that the reader is already familiar with the basics of OS/2, C programming, EMX and has access to the whole EMX docs (unresolved references within this doc point to them, if I'm not mistaken). If you haven't read the latter and tried out all supplied examples better do this now! If you can't handle gcc or write simple Makefiles you have to learn your lessons first! This FAQ is not a "Beginners Guide to un*x/programming", though I address several topics from such an introduction.

Since it may be rather hard to actually detect something helpful in this document, so you should remember that the FIND function of your browser/viewer is your friend: Since quite a lot interfaces and packages are covered here, just look for a keyword and hope there's already a helpful comment on it! Some parts have meanwhile been put in external documents with "local linkage"; official "compiled" versions (PDF currently) may include those for your convenience.

In case you may want to correct an error, contribute another entry or "just" correct spelling/grammar, please check first the primary location of this FAQ for the latest revision. You may send me emails with corrections, enhancements and criticism!

Copyright, License, Legal Stuff

Contributions to this document came from various people and resources on the net. However to avoid that a single one is neglected in a reference list, all are omitted! Two exceptions are being made for two people without their efforts I would not use OS/2 anymore and therefore not work on this FAQ: Eberhard Mattes (author of the EMX package including the docs which I frequently cite) and Holger Veit (who ported XFree86 to OS/2). Many hints given here came from these two people and the numerous members of their projects' mailinglists.
This document is being made available as copyrighted freeware (with copyright holder Alexander Mai) without asking all those contributors (which may not be very nice from me; however I think I don't really violate a specific license). This means you can get this document and re-distribute it for free or put excerpts in another work as you want. But if you re-distribute the whole FAQ you have to mention my copyright notice in a credits section :-)
Especially you shouldn't make too much money selling this FAQ without sending me at least the half of it ... ;-)

Note that usage of the tips & procedures described here is totally on your own risk, there is no warranty that they do what one might expect without any (unwanted) side effects!

News

Since I don't get much feedback concerning this document it is not only growing very slowly but also still full of typos and other errors.

Work is ongoing to get many of the hints and workarounds mentioned here into a real powerful extension library for EMX. Take a look and see what's already done! Especially try to look up whether symbols missing in this FAQ are already included in libExt.

There is a PDF version of this document available. From the PDF format you can easily get a fine printout. Using appropriate tools you can also search in it and extract a plain text version. (Check out pdftotext, which is part of the xpdf distribution for that purpose!)

Prerequisites

To actually port something EMX and all important related development packages have to be installed properly (in their latest version). In case you want to deal with X11 applications you need the full XFree86OS/2 package including the development stuff (all alternatives are outdated and therefore neglected here). Also see libext. Furthermore additional tools will be necessary in many cases.
Due to the substantial number of external links within this FAQ access to the Internet is nearly mandatory.

Useful & Dirty Workarounds

These are not necessarily "canonical" and clean approaches but often they will fullfill their purpose very well. This section includes functions, macros and constants. The functions/macro replacements might be "enhanced" by adding the correct number of arguments. Obviously defining a macro/function to either 0 or the appropriate return code upon error relies on the concept that the source was written properly and to some extent handles failures of these APIs properly.
Attention: using these redefinitions will make debugging more complicated since you have to remember all of them! At least I don't have a source code debugger which offer additional support here.

#define chdir              _chdir2           /* see below */

#define chown(x,y,z)       (0)

#define endgrent()                           /* is type void */

#define FNDELAY            O_NONBLOCK

#define getcwd             _getcwd2          /* see below */

#define getdtablesize ()   (1024)

#define getgrent()         ((group*)NULL)

#define link(x, y)         (-1)              /* no hard links */

#define lstat              stat              /* no soft links */

#define major(dev)         (0)               /* doesn't make sense on OS/2 */

#define makedev(dev,minr)  (0)               /* doesn't make sense on OS/2 */

#define minor(dev)         (0)               /* doesn't make sense on OS/2 */

#define mkfifo(p,m)        (0)               /* see below for mkfifo() */

#define mknod(p,m,d)       (0)

#define mlock              (-1)              /* see below */

#define munlock            (-1)              /* see below */

#define munlockall         (-1)              /* see below */

#define readlink(x,y,z)    (0)               /* or a negative value and use errno? */

#define readlink(s,t,l)    (strcpy(t,s),strlen(t))

#define setgrent()                           /* is type void */

#define setpgrp()          (0)               /* ? */

#define sigset(x,y)        signal(x,y)       /* well, almost ... */

#define strcasecmp         stricmp           /* see libExt */

#define strncasecmp        strnicmp          /* see libExt */

#define symlink(x,y)       (-1)

#define sync()                               /* check out fsync(), etc. */

#define S_ISLNK(x)         (0)

#define S_IFLNK            (0)

#define S_ISBLK(x)         (0)

#define S_IFBLK            (0)

#define S_ISVTX            (0)

#define usleep(t)          _sleep2(((t)+500)/ 1000)   /* see below */

APIs that need Special Care

This section features some comments on available APIs which have to be handled with special care and some thoughts on not yet implemented ones.
ecvt(), fcvt(), gcvt()
Convert a floating-point number into a string. Not available AFAIK. Try replacing it by *printf() or even attempt writing your own version. Shouldn't be too hard ...
errno
errno is nothing un*x-specific, but an integral part of ANSI/ISO C's error handling. The catch here is that some programmers erroneously #define errno to something like int or abuse it as a name for an identifier.
Related to errno is an important thumb-rule: always compile and link your EMX apps as multi-threaded applications (gcc flag -Zmt). This will avoid much trouble.
exec*()
See fork() and the section about process handling.
Replace exec*() with spawn*() and exit() if the parent process waits for the termination of the new process (by calling wait() or by waiting for SIGCLD). This is required to keep the process ID of the child process. In a forked process, however, you don't have to do this because emx.dll does it for you.
fchmod()
Part of libext.
fcntl()
F_SETLK is not supported.
Write your own fcntl() or add code which makes use of an existing flock*() implementation. Or watch libext.
flock()
Check out the native API DosSetFileLocks(). Also see libext.
fork()
See exec*() and the section about process handling.
Replace fork() and exec*() with spawn*(). Under OS/2, fork() is inefficient. You may not care if it's rarely being called, but read the docs for any specific information about the fork() implementation.
Check the section about XTHREADS if you are going to use fork() and/or threads within an X11 application!
getenv()
see section about environment variables
getrlimit()
This is not a POSIX.1 function, but should be part of libExt
Also see ulimit(3), sysconf(3), emxdev: chapter "7 Customizing"
mlock(), munlock(), munlockall()
The DevHlp() interfaces (API for device drivers) provide such calls, to mark memory pages as non-swappable. However this is no trivial replacement which could be implemented by inserting a few lines of code.
poll()
A call similar to select(). Check out libExt for an emulation using this relationship.
popen()
See system()
rename()
rename() doesn't replace existing targets.
It also fails on open files. (also see remove())
remove()
remove() fails on open files.
On un*x you can (re)move open files. On OS/2 you can't and therefore this call might fail. (This is, a simplified picture; it can also fail on un*x - but won't do that often). A major component here is the file system, and since modern operating systems can deal with many of them, it is not precise to make statements based only on the OS being used.
select()
select() is a common source of portability problems. These may involve: the necessary header files, argument types or some "EMXisms", like the following: select() will report that stdin is "ready", i.e. will return immediately. If you want to trigger on any new input, i.e. keystrokes, you have to change the terminal settings. A small snippet should give you the idea:
#include <sys/types.h>
#include <stdio.h>
#include <termio.h>
#include <termios.h>
#include <sys/time.h>
#include <sys/select.h>
...

fd_set rfds;

struct timeval tv;

int retval;

struct termios tio;

...

/* Watch stdin to see when it has input. */

FD_ZERO(&rfds);

FD_SET(fileno(stdin), &rfds);

/* Now change EMX' default settings */

tcgetattr(fileno(stdin), &tio);

tio.c_lflag &= ~IDEFAULT;

tcsetattr(fileno(stdin), TCSANOW, &tio);

/* Wait up to five seconds. */

tv.tv_sec = 5; tv.tv_usec = 0;

retval = select(1, &rfds, NULL, NULL, &tv);

if (retval > 0) {

   /* FD_ISSET(0, &rfds) will be true here */

   printf("Data is available now.\n");

}

sigaction()
Check the code which signal model is used and chose the appropriate linker flags (see signal()). Especially check if you need to link with -Zbsd-signals while XFree86 OS/2 (imake; perhaps the docs mention it as well) tends to use -Zsysv-signals!
Also read the docs what happens if sigaction() and signal() are used together.
signal()
By default EMX' signal processing is different when using signal(): SIG_ACK should be used instead of the signal handler address to re-enable a signal by calling signal() when the signal handler has been called. This behaviour can be changed with the -Zbsd-signals and -Zsysv-signals options of gcc. If you use POSIX.1 functions for signal handling, SIG_ACK is not required.
Comment out or replace code which is based on non-implemented signals. The EMX (IBM toolkit) docs list all available signals and give further information. By collecting signal types from various operating systems one gets a rather long list. Actually it became fun to collect signals from various sources. You will rarely find a longer reference list anywhere!
Note that the references {OS/2-IBM, EMX, Both, none} may be incomplete or even wrong. Please check the docs, or even the headers.

Collection of signal types
SIGNAL name Short Explanation Reference Comments
SIGABRT Process abort signal B e.g. by abort()
SIGALRM Alarm clock E  
SIGBREAK CTRL-BREAK by user B OS/2 only
SIGBUS Bus error E  
SIGCHLD (SIGCLD) Child process stopped/terminated E  
SIGCONT Continue if stopped -  
SIGDANGER Danger signal -  
SIGEMT ? E  
SIGFPE Floating point exception B  
SIGHUP Hangup E  
SIGILL Illegal instruction B  
SIGINFO Information request -  
SIGINT Interrupt from terminal B  
SIGIO IO now possible - (4.2 BSD)
SIGIOT IOT trap -
SIGRANT Monitor mode granted -  
SIGKILL Kill process E cannot be be caught or ignored
SIGLOST Resource lost -  
SIGLWP ? -  
SIGMSG Monitor mode data available -  
SIGNOFP Floating point co-processor not available -  
SIGPHONE ? -  
SIGPIPE Broken pipe E write to pipe w/o reading process
SIGPOLL I/O possible -  
SIGPROF Profiling timer expired -  
SIGPWR Power failure -  
SIGQUIT Quit from terminal E  
SIGRETRACT Need to relinguish monitor mode -  
SIGSEGV Segmentation fault B  
SIGSOUND Sound completed -  
SIGSTKFLTT Stack fault on coprocessor - (linux)
SIGSTOP Stop process -  
SIGSYS Illegal system call E  
SIGTERM Termination B  
SIGTRAP Breakpoint/tracepoint trap E (see "hardcoded breakpoint")
SIGTSTP Stop typed at tty -  
SIGTTIN tty input for background process -  
SIGTTOU tty output for background process -  
SIGUNUSED Unused signal - a very important one ;-)
SIGURG Urgent I/O condition -  
SIGUSR1 User defined signal #1 B  
SIGUSR2 User defined signal #2 B  
SIGUSR3 User defined signal #3 I EMX seems to miss this!?
SIGVTALRM Virtual timer expired -  
SIGWINCH Window resize E see termsize lib
SIGWIND ? -  
SIGXCPU CPU time limit exceeded -  
SIGXFSZ File size limit exceeded -  

system()
If you are lazy and keep the un*x-like forward slashes in paths be aware that arguments to system() may be passed to cmd.exe which might not be able to handle them!
system() and popen() do not expand wildcards unless COMSPEC/EMXSHELL points to a shell which expands wildcards (of course on un*x those calls wouldn't do either - but the shell being called). Check out the EMX docs to learn which shell is called by these routines! One may run across code like system("foo&"); which would try to launch a job in the background with an un*x shell but this won't work with cmd.exe!
unlink()
An obsolete call. Use remove() instead.
usleep()
The workaround given above (using _sleep2()) doesn't feature a microsecond resolution; but contrary to its name many implementations on un*x won't do either. More info on timers on OS/2 is available on EDM/2. Also see info about g/setitimer().
Another quite "portable" approach is using select() (see according entry in this FAQ). You have to specify no descriptors to monitor but only a timeout. Check out the docs which resolution actually is provided here and about properties of this call in general (e.g. the effect of signals beign raised).
#include <sys/types.h>
#include <sys/time.h>
#include <sys/select.h>


struct timeval tv;

long secs, microsecs;

/* set secondss and microseconds of the interval */

tv.tv_sec  = secs;

tv.tv_usec = microsecs;

/* tell select() to monitor no file descriptors */

select(0, (fd_set *)NULL, (fd_set *)NULL, (fd_set *)NULL, &tv);

Ported APIs

This is a collection of known ports of APIs (API sets), i.e. stuff which is more complex than a simple
#define foo bar
I try to list only "general purpose" stuff that can used in any kind of application. Interfaces for rather special purposes may be collected elsewhere. Some of those and many others are meanwhile part of libExt.

un*x Process Stuff

Process handling is a bit different on un*x. However often you don't notice a difference, since the most frequently used APIs are dealing with starting a process via system(), exec*(), etc. which are available on OS/2 EMX (also see fork()).

The term threads has no unique meaning in the un*x world as opposed to OS/2. Those un*x systems which had already offered something similar to the OS/2 threads concept often developed their own standard for this. pthreads (POSIX threads) is now a kind of standard. A wrapper from this API set to OS/2 exists (see Hobbes). For the purposes similar to those of multi-threading often processes started via fork() are being used.

When fork()/ exec*() is used to start a subprocess often communication between the parent and child process is done. The most simple way is to redirect stdin/stdout/stderr using pipes (created by pipe()). If you migrate the code from fork()/ exec*() to spawn*() you have to change this code slightly as well: in the former case pipes are created, then the child process switches it's std* to the according ends of the previously created pipes before calling exec*(). The parent just closes the unused ends of the pipes and keeps those required to communicate with the child.
Using spawn*() you don't have explicit access to the child, but it inherits the pipes/handles given from it's parent. So you have to redirect std* of the whole process to the pipes, start the child and fixup std* again:

  1. make backups of the std* handles (-> dup())
  2. redirect the std* handles (-> dup2())
  3. start the child (-> spawn*())
  4. restore std* (-> dup2()) and close() unused file handles

Watch out for open file (socket, ...) handles being passed to subprocesses: e.g. open file handles passed to a spawned child may prevent the parent from removing that file later in the future. See fcloseall() and fcntl() (-> FD_CLOEXEC flag)

un*x Filesystem Stuff

Common "native" un*x filesystems are different from those usually found on OS/2 (HPFS, HPFS386, JFS). I try to list some major differences here. Be aware that some surprising things may show up: interfaces to processes and hardware devices for example!

Misc Issues

This is a collection of various helpful suggestions which didn't fit in the other sections.

In & Out

This is a section about genera Input/Output related topics. This is rather complex and I can't even nearly fill/discuss all sections

Printing

Printing on un*x is quite different to OS/2: no sophisticated system interfaces exist (read: may exist but are rarely used) so all applications have to this on their own (neglecting e.g the "new" X11 printing interface, which in turn may be used by libs such as M*tif. Common denominator is that a Postscript-capable printer is supposed to be attached to the system. And a set of standard utilities to submit/manipulate jobs for this device.
If you're lucky enough to have a Postscript printer on your OS/2 system you may use the lpr and lpd tools (shipped with Warp 4) to access the Postscript printer queues from the commandline.
More on this topic can be found in a separate document about printing from non-PM applications.

Sound

Portable audio doesn't seem to be available across operating system borders. For many un*x flavours there's the Open Sound System (OSS), which brings digital audio (also MIDI) with an "open" interface.
There's a /dev/audio emulation available from Hobbes and LEO (devaudio*.zip). Given this version one might imagine to have the current implementation enhanced by additional features.

Miscellaneous I/O

With respect to general hardware access it's hard to find a portable standard at all here.
Here's a short list of docs/projects which deal with portable I/O:

EMX Specifics

[most parts of this chapter should be removed and the content placed at the proper sections of this document.
Ok, work has begun now, topics covered elsewhere just get deleted here!]

From EMX docs

The following entries are more or less excerpts from the EMX docs. However I started to modify them and therefore do no longer claim that it's actually the same what you can read in the EMX distributions!

from 5.1 Porting Unix applications

from 5 Hints for porting Unix programs to emx

From other sources

Configuration Stuff

Various build mechanisms are used in the un*x world: simple/complex/nested Makefiles, crazy batch files, autoconf, Imakefiles, ...
Anyway you should get all available un*x tools which are already ported, most of them being from GNU. More details can be found at the description of autoconf and in the tools section.

Makefiles

Batch Files

autoconf

Imakefile

libtool

is used to build libraries. Usually only for shared libs, since producing static ones should be quite simple (and standard).

Tools

Standard Tools

Except the basic requirements you need some additional tools if you want to build packages bigger than the famous "Hello_World.c" ...
The following list is probably too long, but except a waste of disk space I see no reason not to install some tools in advance ;-)
Those tool have to be first! in PATH before any other incompatible versions.
All should be available from Hobbes, LEO. Don't forget to put sh.exe into the /bin subdirectory on your working drive to keep many scripts/Makefiles running without changes. (this holds for other standard tools as well, like rm, cp, ...)
Other useful tools include:

Scripting Languages

un*x offers a variety of script languages like perl and Tcl/Tk. Some are used for rather simple tasks if the simpler tools (like sed, awk, etc.) are no longer sufficient or the suitable commands become too awful. The OS/2 command shell cmd.exe can't execute such things directly, so you either have to explicitly call the interpreter (like foo bar.foo) or rename it to foo.cmd and add a suitable first line containing extproc foo like e.g. for perl scripts:
extproc perl -Sx

Note that this kind of perl scripts have to be in your PATH

The following table lists several language interpreters which have already being ported to OS/2. Those may not necessarily be the latest version, but you will find out yourself.
Script languages and their OS/2 ports
Language OS/2 port
awk (e.g. GNU awk="gawk") Hobbes, LEO
m4 Hobbes, LEO
perl ftp.math.ohio-state.edu or the old distribution from Hobbes, LEO
python www.python.org and another page
Tcl www.vaeshiep.demon.nl
Tk www.vaeshiep.demon.nl

Archiv Formats

Here is a list of some common archive formats and the according tools to extract them (also see Hobbes, LEO)
Note that on un*x it's standard to use cumulative suffixes, so hello.tar.gz refers to a file hello which was produced by tar and finally packed by gzip.
Archive formats
extension related tool
.cpio cpio
.deb Package for Debian linux
.bz2 bzip2
.gz (GNU) gzip (gzip -d, gunzip)
.rpm RPM; OS/2 port of this tool
.shar sh (shell archive) (sh foo.shar)
.tar (GNU) tar
.uue uudecode/uudeview
.Z decompress/gunzip
.z pack/unpack
.zip Info-(un)zip

Helpful Stuff

X11 Specifics

In general you don't need to make changes specific related to the XFree86 OS/2 implementation of the X11 standard. It is very close to the level which is supplied for un*x platforms, like linux. In turn XFree86 is based on the official releases from the OpenGroup and therefore conforming to the standards.
In fact most problems arising are already covered in the XFree86 OS/2 FAQ and on the related mailinglist, which has an online archive.

How to get a single key pressed in an xterm?
This is a real FAQ for un*x programming (check the links section for the according FAQ compilation!). By default many terminals are in a mode which blocks until RETURN is pressed. The major task to be done is therefore setting the terminal in the correct mode. Fortunately there is a standard interface for this defined within POSIX.
[Feel free to finish this section! Or just have a look at some sample code for now.]
How to work around the non-working SIGWINCH?
Have a look at this "termsize" library
Is X11 multi-threaded?
The OS/2 port of XFree86 does not support the X11 threading concept called XTHREADS. This itself is no major limitation since any valid software has to (and can easily do) check whether this feature is enabled or not.
It is important to make any X11 calls from a single thread. Otherwise you may get errors like: "Xlib: unexpected async reply"
How to debug an X11 application?
See below for some general comments. Remember that an X11 application isn't running straight through some main() routine (except for the beginning or end, perhaps), but most of the time it stays in a loop which waits for events like keyboard or mouse actions. Then it calls the appropriate routines (callbacks) which have been installed to react upon them.
How to resolve X errors?
If the app foo is linked against Xt.dll run foo -sync inside a debugger and set a breakpoint on exit() (see below). There are usually static X11 libs available. Linking against these might help in debugging (no, it does not really help, it just makes life easier, since all symbols are now within the executable which is handy when using a debugger).
What if an (X11) app fails to link due to unresolved symbols?
If the linking process fails, this probably means you didn't specify some library, or the required libraries aren't present on your system (this is nothing X11 specific). Also you could have missed to specify an object file, of course.
Error messages indicating this contain long lists of undefined symbols, most of which have a name with an identical prefix, such as Xm. The leading underscore _ which gets added to all symbols (at least using EMX/gcc) is omitted here for brevity.

In the following I list some prefixes and the linker command to select the appropriate library. The "Origin" entry tells you which package supplies the appropriate library.
X11 related libraries
Prefix Library Origin
X -lX11 XFree86 OS/2
Xt -lXt XFree86 OS/2
Xmu, XEditRes -lXmu XFree86 OS/2
Smc, Sms -lSM XFree86 OS/2
Ice -lICE XFree86 OS/2
Xdbe, Xext, XShape -lXext XFree86 OS/2
Xie -lXie XFree86 OS/2
Xp -lXp XFree86 OS/2
XScreenSaver -lXss XFree86 OS/2
XTest, XRecord -lXtst XFree86 OS/2
Xaw -lXaw XFree86 OS/2 (?) (also check OS/2 ports)
Xm -lXm M*tif (LessTif)
Mrm -lMrm M*tif (LessTif)
Uil -lUil M*tif (LessTif)
Xbae -lXbae Xbae (matrix widgets) (LessTif)
Xpm, xpm -lXpm XFree86 OS/2, ports
fl_ -lforms xforms (OS/2 ports)
fltk -lfltk fltk (OS/2 port)
gl_ -lMesaGL MesaGL (OS/2 ports)
? -lgtk ?
shm -lshm various sources
? -lqt ?

You can locate symbols not listed here by running emxexp on the libraries in %X11ROOT%/XFree86/lib. Obviously the library name tends to be the same cryptic letter combination as the prefix you are looking for ...

The order in which you have to link these libs is partially fixed:

-lUil -lMrm -lXm -lXt -lXmu -lXp -lXext -lXpm -lSM -lICE -lX11

Of course not all of these libraries are always necessary, but in some cases even more libraries may be required. The socket library (-lsocket) is another candidate here. It provides e.g. gethostname() or connect().
How to link an X11 application statically?
If you want to debug an X/2 application and need to deal with X Errors (see debugging.html and debugging_os2.html) you will appreciate to link the X11 libraries statically as well.
To do so you have to get the static libraries package from the official distribution. Then replace all link requests -lfoo by -lfoo_s. Switching one library from dynamic to static linkage might force you to do the same with others as well! However you may try to link only those statically which are required to satisfy the linker (see example below). In addition new libraries might now be requried, e.g. -lsocket (see "What if an (X11) app fails to link due to unresolved symbols?"). A sample commandline would be
gcc -Zmtd -Zbsd-signals -o bar.exe bar.o -lXt_s -lSM -lICE -lX11_s -lsocket


Be warned though: statically linked X11 apps might cause problems once you update your X/2 system. Also my instructions might be incomplete: I got some strange errors which I could not explain so far ...
How to create a "dualmode" application?
If building a dual mode application (command line and X11 app in a single executable) you may wish to link to the X11 libaries statically. This way you can distribute a binary to people which don't have XFree86 OS/2 installed.
Actually I should better say "stand-alone, dualmode" application, therefore see also "How to link an X11 application statically?"

Debugging

Well, debugging is nothing really specific to porting. You will need to look for bugs in code written for any platform ...
But you may also run across new types of programs which demand additional knowledge. I have written two very short summaries about that topic:

Online Resources

There are many link lists out in the WWW. I try to collect real helpful ones, i.e. canonical resources as well as sites which have real unique offers.
Send Feedback to
Alexander Mai ( <st002279@hrzpub.tu-darmstadt.de> or <amai@lesstif.org> )
Last modified on 20010105