Porting cookbook - Onstor Software Engineering A. Sharp Wed Apr 4 18:55:44 PDT 2007 1.0 Creating a portable code base v. something that compiles in multiple environments. The initial temptation when porting some code is just to get it to compile by adding ifdef's here and there. This works, but if you ever have to port the code again, you have not gained any ground for your effort. The idea then is to make the code portable instead. This means creating a framework whereby the code can easily and quickly be ported again to another platform or environment (simulator?). The trick to making code portable is to not use ifdefs, but rather use wrapper functions and macros, and have the makefile system fix everything up for you automagically. The next section contains an example. 1.1 Example porting code using wrapper technology Suppose you have a code snippet that calls a system call that has different arguments on OpenBSD and Linux. Furthermore, it has different meaning for the return value as well. So you have to handle both of these. Let's say our system call, sys_foo() has the following symantics on the two operating systems: OpenBSD: int sys_foo(struct fubar *bar); where the return type is a value that you want, or -1 if there was and error. Linux: int sys_foo(struct fubar *bar, int mask, int *val); where the return value means 0 for success, non-zero for failure, and the value that you want is returned in the variable pointed to by val. You have the following bit of code you now need to make portable: int val; struct fubar bar; int rc; val = sys_foo(&bar); if (val == -1) { rc = errno; val = 0; } else { rc = function(val); } return rc; You could use ifdefs: int val; struct fubar bar; int rc; #ifdef OPENBSD val = sys_foo(&bar); if (val == -1) { rc = errno; } else { rc = function(val); } #elif defined(LINUX) rc = sys_foo(&bar, 0xdeadbeef, &val); if (rc != 0) { rc = errno; } else { rc = function(val); } #endif return rc; But then the next time you need to port this code, you would have to do the work all over again, changing all your main source files. To make it portable, you would use a wrapper function instead. Here I've used a function of the same name except with "os_" (for OnStor) prepended to indicate what the function is and that this is a wrapper around it. Adopting a consistent naming scheme for wrappers will help maintainers to identify wrapper functions vs. regular functions and macros. int val; struct fubar bar; int rc; rc = os_sys_foo(&bar, 0xdeadbeef, &val); if (rc == 0) { rc = function(val); } return rc; In OpenBSD the wrapper function would be: /* * I'm reusing the 'mask' argument because we don't use it in OpenBSD */ int os_sys_foo(struct fubar *b, int mask, int *v) { mask = sys_foo(b); if (mask == -1) { mask = errno; *v = 0; /* just in case -- we don't know what sys_foo does */ } else { *v = mask; mask = 0; } return mask; } In Linux, the wrapper function would be: int os_sys_foo(struct fubar *b, int m, int *v) { int rc; rc = sys_foo(b, m, v); if (rc != 0) { rc = errno; *v = 0; /* just in case -- we don't know what sys_foo does */ } return rc; } Now, in this rather simple case, you could also have used a macro, since all the variables necessary for both versions already exist in the function. So you probably get the idea, and you've probably done something like it before. The question remains, however, how do I get the right wrapper function selected and compiled and linked in without an ifdef somewhere? 2.0 Makefiles The basic principal is that the makefiles know all, and do the heavy lifting for you. The way this works is that in any directory where you have a logical building module, say nfx-tree/code/ssc-nfxsh, for example, you might have any of the following files as necessary: Linux.h OpenBSD.h Linux.c OpenBSD.c Your wrapper functions (unless they are inlines) would go in the respective .c files, and macros and other selective includes would go in the .h files. The .h files are heirachical, so that macro and inline function wrappers that are universal or widely utilized, would be defined in a higher level .h file of the same name, and the lower level file will always include the higher level file. So, in nfx-tree/code/ssc-nfxsh, the Linux.h file, if there is one, would have the following include: . . #include . . Assuming, that is, that the path where that file is found is part of the include search list specified by the -I compiler option. In our case, we probably only have one higher level, and that's the Includes directory, but I could be wrong. If there is more than one level, each include file only includes the next higher level file. The first part of the name of the files you'll notice I've capitalized, but isn't necessarily how we'll do it. However, that is handy if you are using the uname command in the makefiles to determine which system you are on, without using ifdefs in the Makefile: PLAT=`uname -s` include $(PLAT).mk or, if there is no local $(PLAT).mk, then this would be include $(TOP)/$(PLAT).mk otherwise the local $(PLAT).mk would include $(TOP)/$(PLAT).mk The $(PLAT).mk file would have some of the following in it: CFLAGS += -DPLAT_INC="$(PLAT).h" and SRCS += $(PLAT).c In a .c file that is a main source file, you would have the following include: #include PLAT_INC That way, the main source file would include whichever .h file was appropriate for the operating system platform it was being built for. We are cross building, so we won't be using uname right now; instead we'll just set the make variable PLAT based on an environment variable or a for loop building the source for both platforms. Depending on how our makefiles currently operate, they may have to be modified to output binaries to other directories (subdirectories or a different tree) so that you could build for both platforms with fear of mixing things up. The following section shows an example snippet that might appear in a makefile. 2.1 Example makefile snippet SRCS = foo.c bar.c LIBS = $(LIBS) include $(PLAT).mk BINS = $(SRCS: $(PLAT)/$(PROD)/%.o=%.c) $(BINS): depends .c.o: $(CC) $(CFLAGS) $< -o $(PLAT)/$(PROD)/$@ $(PLAT)/$(PROD)/foo: $(BINS) $(CC) $(LFLAGS) $(BINS) $(LIBS) -o $(PLAT)/$(PROD)/foo install: $(PLAT)/$(PROD)/foo install $(PLAT)/$(PROD)/foo $(INSTALL_DIR) I'm told that our makefiles already have the separate build directories issue under control. 3.0 Summary This forms the basics of a portable code base/framework. Then, if you have to port your code again in the future, you simply have to add the .h and .c files for your new platform, and it all just works(tm). You don't have to modify your makefiles or anything else. Sweet. 3.1 More complicated example If you have a directory where more than on module is built, or is very complicated, you may wish have your platform specific files in a subdirectory. This means that instead of having your Linux.[ch] and/or OpenBSD.[ch] files in the immediate directory, you have a directory/file layout something like this: source directory/ src-file-1.c src-file-2.c src-file-3.c Linux/ Makefile foo.c bar.h Linux.h OpenBSD/ Makefile foo.c bar.h OpenBSD.h Then, the makefile might look something like this SUBDIRS += $(PLAT) .PHOHY = subdirs subdirs: for d in $(SUBDIRS) ; do make -C $$d || exit 1 ; done or, it might look like this: SRCS += $(PLAT)/foo.c and then as before in section 2.1. :vim set wm=10 :vim set ai