Chris Zakelj wrote:
I'm curious as to how programs actually get ported from one OS to
another,
Yes, some techniques make the job easier, but it depends on what the
program does and whether you're doing a one-way port or an ongoing port.
The following aren't necessarily the only ways to do porting, but it's
how the Open* Portable projects are done.
If it's a one-way port (ie the port will be done once and thereafter
maintained separately), the usual method is to just change what needs
changing to suit the target platform. This is effectively what happened
when the original ssh-1.2.12 code was 'ported' to OpenBSD.
In the case of OpenSSH and OpenNTPD Portable, they're ongoing ports (ie
changes are regularly sync'ed from OpenBSD's to Portable's). There are
2 main ways to accomplish this: sprinkle the code with #ifdefs or
implement the missing functionality in a compatibilty layer.
In the Portable projects, the first preference is leave the common code
alone and implement the required functionality in a portability layer.
This has the advantage of keeping the common code clean and if done
properly the components are reusable. (A number of functions used by
OpenNTPD Portable came unmodified from OpenSSH Portable). Sometimes
that's not possible or more effort than it's worth, so in those cases an
#ifdef is used which imposes an ongoing maintenance cost (ie next time a
change is made in that area in the main code, you'll have to manually
resolve conflict when syncing changes).
For example: OpenNTPD Portable was ported to run on QNX4 (a POSIXish
embedded system) by Anthony O.Zabelin. The 2 main missing pieces were
the adjtime() and poll() calls. Simplifying somewhat, the code that
used adjtime looked like this:
d_to_tv(d, &tv);
if (adjtime(&tv, NULL) == -1)
log_warn("adjtime failed");
If we had used the #ifdef technique, that would have changed to
something like this:
#ifndef __QNX__
d_to_tv(d, &tv);
if (adjtime(&tv, NULL) == -1)
log_warn("adjtime failed");
#else
usec = (int)(d * 1000000);
if (qnx_adj_time(usec, ADJUST_RATE, NULL, NULL) == -1)
log_warn("qnx_adj_time failed");
#endif
Now one or two of those aren't too bad, but it rapidly becomes difficult
to follow once you add a few more to the same piece of code.
Instead, Anthony wrote a stand-alone replacement adjtime() function
which is in the portability layer (openbsd-compat/port-qnx.c). This had
a higher initial cost (it's 23 lines of code in a single function plus a
Makefile change instead of 6 lines listed above) but it leaves the main
code unchanged. It can also be tested separately and is reusable.
I took the same approach with poll() and built a replacement on top of
select(), which QNX4 did have. Hey presto, it now worked on QNX4, and
the codebase is no harder to maintain.
and if certain directions are easier than others.
In general, the difficulty is directly proportional to how different the
target platform is compared to the platforms already supported in the
area in which the program operates. In most current OSes there's a slow
convergance toward common APIs for standard languages so if you stick to
those standard APIs you life will be easier.
Newer/more featureful -> older/less featureful is usually harder than
the other way around unless the program was originally written to stick
to a common subset.
Beyond that it depends on the program. Porting a simple text filter
from a bleeding-edge Linux to 10-year old BSD is likely to be simple,
but other programs may be difficult to impossible.
That is, how does one figure out what needs to be changed in order to
make OpenNTPD work on Linux?
I had the advantage of having worked on Portable OpenSSH for a couple of
years so had a reasonable idea what to expect, so for OpenNTPD I just
copied the code onto a Linux box, hacked the BSDisms out of the Makefile
and tried to compile it. This highlighted some obvious problems (eg
missing strl* functions, the lack of sa_len in struct sockaddr). I
fixed these (stole the strl* functions from OpenSSH and changed
sa->sa_len into SA_LEN(sa)) and tried again. After a few iterations of
this process it compiled and after a couple more, amazingly enough, it
worked. At that point I added basic autoconf support, put a tarball on
my web page and mentioned it on [EMAIL PROTECTED]
After that, other folks and myself repeated the process on other
platforms, slowly expanding the list that it would run on. (The
platform list on openntpd.org is in chronological order, earliest first).
Is it generally easier to move a program from $some_bsd
to $some_other_os, or from $some_other_os to $some_bsd?
Depends on the type of program, and in particular what OS-level services
it uses.
OpenSSH, for example, has to deal with user authentication for which
there is a large amount of variance between platforms, so the diff
between OpenBSD's and Portable's is large.
OpenNTPD has to deal with far less variance between platforms so the
diff is much smaller. (adjtime() is common and the interface is simple,
but if/when it starts compensating for systematic clock skew then that
will introduce a significant amount of platform-specific code, however
most of that can be hidden in the compatibility layer.)
How would you even begin to port something like OpenSSH to a non-Unix
system like Windows?
I would say you would need a POSIX-like compatibility layer (eg external
like Cygwin or implement your own), otherwise you would probably have to
do a one-way port. In that case you could keep the protocol code as is,
but you would probably need to replace large chunks of the rest.
Does the chosen language (C, C++, Java, etc) make a difference
in difficulty?
I don't use C++ or Java so I won't comment on them, but IMO the language
can a make significant difference, but only for some things.
It is possible to write portable C. In the case of OpenSSH, most of the
platform-dependant code is there because the operating system requires
it, not the language, and unless the language provides an abstraction
for exactly what you're trying to do then the choice of language makes
little difference.
For example (and I'm oversimplifying here), validating a user's
password: on a traditional Unix system you encrypt the password and
compare against what getpwnam() returns. If your system has shadow
passwords, though, you need to use getspnam() since the encrypted
password isn't returned by getpwnam() on those. Oh, and some really old
systems based on SecureWare have another function (getprpwnam). (This
is ignoring platform-specific functions and the attempts to standardize
this in libraries such as BSD auth or PAM, which have varying degrees of
success.)
If a language provided a "authenticate_user(user, password)" then that
would help in this case, but most don't seem to (and there are many
other examples of platform-dependant things you would need to do besides
this one).
Anyway, some guidelines to avoid common traps for portable C:
1) write for correctness and clarity.
2) try to stick to standard functions (eg POSIX).
3) be prepared to implement your own replacements for non-standard
functions you use.
3b) or standard functions that aren't available on you target platform.
3c) or standard functions that are broken on your target platform.
4) if possible put all the system #includes in a single header and
include that from all of your source files. Headers vary quite a bit
and it's better if you only have to deal with all that variance once.
5) use the datatypes you're supposed to. eg if you need to store a 32
bit value, use "u_int32_t" not "unsigned int", if you're using it in a
signal handler use "sig_atomic_t" not "int". Check and typedef it
yourself if your platform doesn't.
6) Turn on all the compiler warnings and fix what it warns about. Many
are potential portability problems, even if they haven't bitten you yet.
[This list isn't exhaustive, I'm sure other folks will be able to add to
it.]
When I've built from ports, I can see make files doing
OS detection, but from there (not being a very good coder), I can't
really make out how it changes the code based on that. Any
recommendations for "casual programmer" books would be cool...
A place to start would be this diff:
http://www.zip.com.au/~dtucker/openntpd/patches/ntpd-vs-openbsd.diff
It shows the (small) changes to the OpenBSD ntpd code (3.6.1) to make it
"Portable". The remainder of the changes are in files that are only in
Portable (take a look in openbsd-compat/ in the OpenNTPD portable
distribution).
After that, I suggest downloading the OpenBSD-specific ntpd and the
equivalent Portable one and comparing them (diff -ru is your friend).
It's small enough that it ought to be understandable but it's a real
application that runs on 9 platforms (so far :-).
--
Darren Tucker (dtucker at zip.com.au)
GPG key 8FF4FA69 / D9A3 86E9 7EEE AF4B B2D4 37C9 C982 80C7 8FF4 FA69
Good judgement comes with experience. Unfortunately, the experience
usually comes from bad judgement.