Skip to main content

Pascal-Style Local Functions in C (for Z8 Encore!)

When I was a kid, I used Turbo Pascal. It had one feature that I sorely miss in every embedded C compiler: local functions. Local, as in local to a block. Those become a necessity to maintain decent code performance and avoid the penalty of passing duplicate data via parameters. They also help maintain readable, decently factorized code.

A local function, were C to have it, would look like this:

void fun1(void) {
 int a;
 void fun2(void) {
   a = 0;
 }
}

I hope you see that fun2() is local to the main block of fun1(), and that the variable a is in scope within fun2().

A somewhat less clean, but equally well performing way of accomplishing this would be:

extern int a;
void fun2(void) {
  a = 0;
}
void fun1(void) {
...
  fun2();
}

Now the question remains: where do we actually define the variable a, which is supposed to be local to fun1?

It becomes easy if your compiler supports static frames. Static frames are, as their name implies, allocated statically by the linker, and are overlaid according to the call tree. With usual dynamic frames, automatic variables end up on the stack. It should not be any harder to do with dynamic frames, but I haven't checked it out yet.

ZDS II, the C IDE for Zilog's Z8 Encore! and ZNEO products, supports static frames at the assembler and linker level. A frame ends up defining a near and far segment; for a function called fun1 those segments are called ?_n_fun1 and ?_f_fun1, respectively.

Assuming our code is in a file named file.c, and we're compiling for large model, we get

// file.c
extern int a;

#pragma asm segment ?_f_fun1
#pragma asm _a ds 2
#pragma asm segment file_TEXT

void fun2(void) {
  a = 0;
}

void fun1(void) {
...
  fun2();
}

Here, we manually allocate storage for a in fun1's far frame (due to model being large). This method can be used to bring back the nifty (albeit buggy) feature of ZDS II 4.9.x, which got abandoned and is no more present in ZDS II 4.11.0: arbitrary far/near automatic variables.

The syntax used to look like this:

void fun(void) {
  near int a;
  far int b;
  ...
}

The near/far storage specification was ignored when using dynamic frames, but for static frames it allowed you selecting whether given automatic variable would be stored in near or far memory space. Accesses to far variables take an extra clock cycle per byte, and can bring an extra load penalty in some cases, where the data has to be transferred from far memory to a register using LDX, before being useable by the target opcode.

We implement this functionality as follows:

// file.c

#pragma asm segment ?_n_fun
#pragma asm _a ds 2
#pragma asm segment ?_f_fun
#pragma asm _b ds 2
#pragma asm segment file_TEXT

void fun(void) {
  extern near int a;
  extern far int b;
  ...
}

The main drawback of this method, besides it being prone to typos and suffering from relegating part of the compiler to the wrong side of the keyboard, is that the assembly-level symbols _a and _b really have file scope due to the fact that assembler sees it so. Thus, the canonical way of using this trick is to have a complete tree of nested functions all in one C source file, and having unique names for all the automatic variables that have to be accessed by the local functions.

We shoot two birds with one stone here: we regain the foreign-model automatic variables of ZDS II 4.9.x vintage, and we Pascal-style local functions, which can access the automatic variables present in the scope of the caller's call site. Naturally, this is a hack which should only be used where performance or storage limitations demand it. It could be implemented with an extra C preprocessor.

Comments

Popular posts from this blog

Both INCLUDEPATH and DEPENDPATH are usually needed in qmake .pro project files.

qmake, the build tool provided with the Qt toolkit, converts project files written in its own mini-language to platform-specific Makefiles.

This process includes adding necessary dependencies to the Makefile, so that changes in source files trigger rebuilding of the outputs that depend on said sources.

If your project is spread across directories, you'll likely add an INCLUDEPATH line to your .pro file so that the #include directives look sane -- say #include "library/foo.h" instead of #include "../library/foo.h". This can be done by adding INCLUDEPATH += "../" to the .pro project file.

This, by itself, doesn't cause the files in include directories to be treated as dependencies. This is a sane default, since you likely don't want to rebuild your whole project if a system library changes -- assuming, of course, that the library is meant to stay binary compatible between releases!

Thus, if any of your source files references a file from somewhe…

Asterisk 1.8 with SELinux on RHEL 6 / CentOS 6

Asterisk 1.8 is the current long term support (LTS) version of Asterisk. You can find it in the atrpms repository. Using atrpms requires a bit of ingenuity, since you must enable yum priorities. Here's how I've set up my yum priorities and excludes to play well with RHEL (lower priority is higher):

# /etc/yum/pluginconf.d/rhnplugin.conf
priority = 9

# files in /etc/yum/repos.d - current samba and subversion override those of RHEL
sernet-samba - 5
wandisco - 6
rhnplugin - 9
centos-base - 20, includepkgs=xfs* fftw-* glpk-* dell-firmware-repository - 30 dell-omsa-repository - 30 rpmforge-repo - 50, exclude=hdf5*
epel - 60, exclude=dahdi* atrpms - 70
I'm using a bunch of non-redhat packages, including Asterisk, recent subversion, octave, xfs tools, and Dell server management tools.

For Asterisk proper, I'm running an AGI caller id script, and a fax receiving script. The fax script uses cups to print. Those scripts require exceptions to the targeted SELinux policy. Note that the polic…

Details of Migrating Zimbra 6 to Zimbra 8

I have a small Zimbra system that needed to be migrated from 6.0 to 8.0. Generally:
You must update to 6.0.16 on the source system. The account export functionality is broken in versions prior to 6.0.15 and will fail on some accounts.Start with a fresh 8.0 install. On a small system (dozens of users), that seems like the simplest option.Keep the 6.0 system running. Use the migration wizard in 8.0's admin console (Tools & Migration -> Account Migration) to move over account records only.
Set "type of mail server" to Zimbra Collaboration Suite. Set "would you like to import mail" to No.Import from another Zimbra LDAP directory.Set the LDAP Search Base to "dc=foo,dc=com" where foo.com would be your domain. This saves work on manually moving accounts between systems.
To move account contents and filter settings between servers, you can do the following on the source server:
su - zimbra
ACCT=user@foo.com      # account to move
DEST=root@destination  # ss…