Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Simple dynamic loading example (review requested) -- Am I missing something?

44 views
Skip to first unread message

luser droog

unread,
Jul 31, 2013, 5:55:29 PM7/31/13
to
[reposted from comp.lang.c]

I've avoided learning about dynamic loading for a long time, but
found that I need to learn to do it for things like multiple output
devices and running the whole program as a library call instead of
standalone. (This is all in the context of my Postscript Interpreter,
code.google.com/p/xpost)

So I read http://en.wikipedia.org/wiki/Dynamic_loading
and followed a link to a Linux page, and whipped up this little
example/test. And it works!

But it can't be that easy, right? I've got to be missing something.

Please comment on the following code. (Ignore for the moment
the lack of error handling in the stack implementation; I'm mostly
interested in the interfacing.)

test.c:

/*
Dynamic Loading test
ref.
http://en.wikipedia.org/wiki/Dynamic_loading
http://linux4u.jinr.ru/usoft/WWW/www_debian.org/Documentation/elf/node7.html
*/
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

#include "stack.h"

void fatal(char *msg) {
fprintf(stderr, "Fatal Error: %s\n", msg);
exit(EXIT_FAILURE);
}

struct Stack_interface *si;

void loadplugins (void) {
void *stack_library = dlopen("stack.so", RTLD_LAZY);
if (stack_library == NULL) fatal("unable to load stack");
si = dlsym(stack_library, "interface");
if (si == NULL) fatal("unable to load stack interface");
}


int main() {
loadplugins();
void *stack;
char *a = "A";
char *b = "B";
void *c;

stack = si->stack_init();
stack = si->push(stack, a);
stack = si->push(stack, b);
stack = si->pop(stack, &c);
printf("%s\n", (char *)c);
stack = si->pop(stack, &c);
printf("%s\n", (char *)c);
return 0;
}



stack.h:


struct Stack_interface {
void *(*stack_init) (void);
void *(*push) (void *stack, void *item);
void *(*pop) (void *stack, void **item);
};



stack.c:


#include <stdlib.h>
#include <string.h>

#include "stack.h"
/*
struct Stack_interface {
void *(*stack_init) (void);
void *(*push) (void *stack, void *item);
void *(*pop) (void *stack, void **item);
};
*/

void *stack_init (void);
void *push(void *stack, void *item);
void *pop(void *stack, void **item);

struct Stack_interface interface = {
stack_init,
push,
pop
};

typedef struct stack {
void *item;
struct stack *next;
} Stack;

void *stack_init (void) {
Stack *s;
s = malloc(sizeof*s);
if (s) {
s->item = NULL;
s->next = NULL;
}
return s;
}

void *push (void *stack, void *item) {
Stack *s = stack;
Stack *n = stack_init();
n->item = item;
n->next = s;
return n;
}

void *pop (void *stack, void **item) {
Stack *s = stack;
Stack *n = s->next;
*item = s->item;
free(s);
return n;
}


makefile:

CC=gcc
LDFLAGS=
#LDFLAGS=-rdynamic #Cygwin doesn't need
SHLDFLAGS=

all: test

stack.o: stack.c
$(CC) -c -fPIC $<

stack.so: stack.o
$(CC) $(SHLDFLAGS) -shared -o $@ $^

test: test.o stack.so
$(CC) $(LDFLAGS) -o $@ test.o -ldl

clean:
$(RM) stack.o stack.so test.o test.exe



--
luser droog















Jorgen Grahn

unread,
Aug 1, 2013, 4:17:53 AM8/1/13
to
On Wed, 2013-07-31, luser droog wrote:
> [reposted from comp.lang.c]
>
> I've avoided learning about dynamic loading for a long time, but
> found that I need to learn to do it for things like multiple output
> devices and running the whole program as a library call instead of
> standalone.

Not really: you can also, in many situations, use pipes and separate
processes and executables.

I cannot say for sure if it would apply in your situation, of course
... but on Unix it should be your first choice.

/Jorgen

--
// Jorgen Grahn <grahn@ Oo o. . .
\X/ snipabacken.se> O o .

luser droog

unread,
Aug 1, 2013, 4:40:31 AM8/1/13
to
On Thursday, August 1, 2013 3:17:53 AM UTC-5, Jorgen Grahn wrote:
> On Wed, 2013-07-31, luser droog wrote:
>
> > [reposted from comp.lang.c]
>
> >
>
> > I've avoided learning about dynamic loading for a long time, but
>
> > found that I need to learn to do it for things like multiple output
>
> > devices and running the whole program as a library call instead of
>
> > standalone.
>
>
>
> Not really: you can also, in many situations, use pipes and separate
>
> processes and executables.
>
>
>
> I cannot say for sure if it would apply in your situation, of course
>
> ... but on Unix it should be your first choice.
>


I guess I left out a lot of context. But I see what you're saying. I plan to have various ascii-outputs (plain pbm, pgm, ppm)
but the output device code I want to be "hot-loadable" based on command-line arguments. And I have lofty dreams of running this as a multitasking server, so I want individual threads to be able to load devices by name. And this seems like a neat and tidy
way to do it. Unless I'm doing something stupid and just not
seeing it.

For the other goal of running the whole program as a loadable
library, this is a goal of the secret project being explored
by myself and a few devs from LibreOffice.

https://bugs.freedesktop.org/show_bug.cgi?id=67464

For cross-platform-ability, running the program as a library
service appears desireable. I've just never done it before!
I may further have to wrap it in C++. And it looks like Windows
prefers you to export a function-pointer (whereas Unixes
guarantee the reverse: you can export object-pointers,
not-necessarily function-pointers). So it may (sometimes)
be wrapped in a function exporting this struct.

The Wikipedia page says there's little advantage in packaging
the function pointers like I'm doing. But I think it's much
nicer than calling dlsym for every function. Again, unless I'm
missing something.

Nicolas George

unread,
Aug 1, 2013, 5:09:13 AM8/1/13
to
luser droog , dans le message
<dee4b5d4-7059-4877...@googlegroups.com>, a �crit�:
> I guess I left out a lot of context. But I see what you're saying. I plan
> to have various ascii-outputs (plain pbm, pgm, ppm) but the output device
> code I want to be "hot-loadable" based on command-line arguments.

What actual benefits do you expect to gain from that?

If your answer is "reduce memory consumption" or "reduce loading time", you
are probably wrong: all modern operating systems do on-demand loading for
executable data.

> And I have lofty dreams of running this as a multitasking server, so I
> want individual threads to be able to load devices by name.

That would make it exponentially harder to do without concurrency bugs.

> For the other goal of running the whole program as a loadable
> library, this is a goal of the secret project being explored
> by myself and a few devs from LibreOffice.
>
> https://bugs.freedesktop.org/show_bug.cgi?id=67464

I do not see the link between spawning GhostScript and running your program
as a library.

> For cross-platform-ability, running the program as a library
> service appears desireable.

Please explain.

luser droog

unread,
Aug 1, 2013, 5:52:44 AM8/1/13
to
On Thursday, August 1, 2013 4:09:13 AM UTC-5, Nicolas George wrote:
> luser droog , dans le message
>
> <dee4b5d4-7059-4877...@googlegroups.com>, a écrit :
>
> > I guess I left out a lot of context. But I see what you're saying. I plan
>
> > to have various ascii-outputs (plain pbm, pgm, ppm) but the output device
>
> > code I want to be "hot-loadable" based on command-line arguments.
>
>
>
> What actual benefits do you expect to gain from that?
>
>
>
> If your answer is "reduce memory consumption" or "reduce loading time", you
>
> are probably wrong: all modern operating systems do on-demand loading for
>
> executable data.
>
>
>
> > And I have lofty dreams of running this as a multitasking server, so I
>
> > want individual threads to be able to load devices by name.
>
>
>
> That would make it exponentially harder to do without concurrency bugs.
>

Concurrency is addressed at the language level with
synchronization primitives (or will be, once I get there).


>
> > For the other goal of running the whole program as a loadable
>
> > library, this is a goal of the secret project being explored
>
> > by myself and a few devs from LibreOffice.
>
> >
>
> > https://bugs.freedesktop.org/show_bug.cgi?id=67464
>
>
>
> I do not see the link between spawning GhostScript and running your program
>
> as a library.
>
>
>
> > For cross-platform-ability, running the program as a library
>
> > service appears desireable.
>
>
>
> Please explain.

Well, Ghostscript itself can be used as a library. So in a
foolhardy fervor of competition, I want to do that, too.
The problem for LibreOffice is that the licences don't
permit ghostscript to be linked as a library.

The purpose here isn't speed or memory, per se (although
of course I hope it won't suffer there), but consistent
operation across platforms.

Nicolas George

unread,
Aug 1, 2013, 6:12:28 AM8/1/13
to
luser droog , dans le message
<942a1933-43b0-400a...@googlegroups.com>, a écrit :
> Concurrency is addressed at the language level with
> synchronization primitives (or will be, once I get there).

Which means you have a lot of unexpected trouble ahead of you.

> Well, Ghostscript itself can be used as a library. So in a
> foolhardy fervor of competition, I want to do that, too.

Ghostscript IS basically a library. Its main use is to provide services to
programs, not directly to users.

> The purpose here isn't speed or memory, per se (although
> of course I hope it won't suffer there), but consistent
> operation across platforms.

You still have not explained what is "consistent" in using dynamic loading.

luser droog

unread,
Aug 1, 2013, 6:47:27 AM8/1/13
to
For reasons of licensing (the exact detail are beyond me),
Ghostscript cannot be included in the LibreOffice installer
to be used as a library. It may be used on various platforms
if present, but it may not be present.

As far as why a library as opposed to an executable program,
it would be consistent with the way LibreOffice handles
similar tasks like rendering svg files. I suppose this doesn't
have to be dynamically-loaded.

Jorgen Grahn

unread,
Aug 1, 2013, 8:06:38 AM8/1/13
to
On Thu, 2013-08-01, luser droog wrote:
> On Thursday, August 1, 2013 3:17:53 AM UTC-5, Jorgen Grahn wrote:
>> On Wed, 2013-07-31, luser droog wrote:
>>
>> > [reposted from comp.lang.c]
>> >
>> > I've avoided learning about dynamic loading for a long time, but
>> > found that I need to learn to do it for things like multiple output
>> > devices and running the whole program as a library call instead of
>> > standalone.
>>
>> Not really: you can also, in many situations, use pipes and separate
>> processes and executables.
>>
>> I cannot say for sure if it would apply in your situation, of course
>> ... but on Unix it should be your first choice.
>

> I guess I left out a lot of context. But I see what you're saying. I
> plan to have various ascii-outputs (plain pbm, pgm, ppm)
> but the output device code I want to be "hot-loadable" based on
> command-line arguments.

That seems like a perfect application for pipes and separate processes,
in the simplest form something like:

popen(output_device, "w")

> And I have lofty dreams of running this as a
> multitasking server, so I want individual threads to be able to load
> devices by name. And this seems like a neat and tidy
> way to do it.

So is the popen()-like alternative. There you also get multiprocessing
for free: if you have CPUs to spare, performance is limited by the
heaviest of the data generation and the output formatting, not the sum
of them.

> Unless I'm doing something stupid and just not
> seeing it.
>
> For the other goal of running the whole program as a loadable
> library, this is a goal of the secret project being explored
> by myself and a few devs from LibreOffice.
>
> https://bugs.freedesktop.org/show_bug.cgi?id=67464
>
> For cross-platform-ability, running the program as a library
> service appears desireable.

OK. You posted in comp.unix.programmer, so I gave a Unix-specific
answer. Dividing programs into cooperating processes may be harder
on inferior operating systems, but I cannot tell.

[snip]

Jorgen Grahn

unread,
Aug 1, 2013, 8:36:51 AM8/1/13
to
On Thu, 2013-08-01, luser droog wrote:
> On Thursday, August 1, 2013 4:09:13 AM UTC-5, Nicolas George wrote:
>> luser droog , dans le message
...
>> > And I have lofty dreams of running this as a multitasking server, so I
>> > want individual threads to be able to load devices by name.
>>
>> That would make it exponentially harder to do without concurrency bugs.
>
> Concurrency is addressed at the language level with
> synchronization primitives (or will be, once I get there).

I suspect it's not as simple as "C++ takes care of concurrency", if
that's what you're saying. Not even if you use the brand new and not
very well-known C++11 primitives.

(The popen() alternative would let the OS handle concurrency --
something Unix does safely and well.)

Scott Lurndal

unread,
Aug 1, 2013, 10:55:54 AM8/1/13
to
luser droog <mij...@yahoo.com> writes:
>[reposted from comp.lang.c]
>
>I've avoided learning about dynamic loading for a long time, but
>found that I need to learn to do it for things like multiple output
>devices and running the whole program as a library call instead of
>standalone. (This is all in the context of my Postscript Interpreter,
>code.google.com/p/xpost)
>
>So I read http://en.wikipedia.org/wiki/Dynamic_loading
>and followed a link to a Linux page, and whipped up this little
>example/test. And it works!
>
>But it can't be that easy, right? I've got to be missing something.
>
>Please comment on the following code. (Ignore for the moment
>the lack of error handling in the stack implementation; I'm mostly
>interested in the interfacing.)

From the interface standpoint, I generally use a common base class and
specialize it in each shared object. For example, I have a hardware
simulator that needs to load a variable set of device models. Each
device model is implemented as a specialization of "class c_dlp",
and a line printer model could be "class c_buffered_printer_dlp: public c_dlp".

Then, each shared object (e.g. libdlp_buffered_printer.so) has an
extern "C" get_dlp function (extern "C" so the name provided to
dlsym(3c) isn't mangled).

/**
* Get an instance of the Buffered Printer DLP
*
* @param name Unused by the buffered printer DLP
* @param channel The channel number assigned to this instance
* @param lp Logger to use for diagnostic output
* @returns a pointer to a c_dlp instance.
*/
c_dlp *
get_dlp(const char *name, channel_t channel, c_logger *lp)
{
return new c_buffered_printer_dlp(name, channel, lp);
}

Then, the code that loads a device model is:

/**
* Each shared object that simulates a Data Link Processor (DLP), will
* contain a single namespace-scope function <b>get_dlp</b> which constructs
* a DLP object of the specified type (for example, a #c_uniline_dlp,
* #c_card_reader_dlp, et alia). <b>get_dlp</b> returns the constructed
* object as a #c_dlp object to the #channel function, which is then used
* by the I/O subsystem to request services of the DLP at the MLI level.
*/
typedef c_dlp* (*get_dlp_t)(const char *, channel_t, c_logger *);

...

handle = ::dlopen(dlpfile, RTLD_NOW);
if (handle == NULL) {
lp->log("Unable to open '%s': %s\n", dlpfile, dlerror());
return false;
}
register_handle(channel, handle);

get_dlp_t sym = (get_dlp_t)::dlsym(handle, "get_dlp");
if (sym == NULL) {
lp->log("Invalid DLP shared object format: %s\n", dlerror());
unregister_handle(channel);
::dlclose(handle);
return false;
}

/*
* Invoke the shared object 'get_dlp' function, passing the
* command line optional one-word DLP-specific argument.
*/
c_dlp *dlp = sym(argv[3], channel, get_log());

get_system()->set_dlp(channel, dlp);

...

I recommend RTLD_NOW instead of RTLD_LAZY to avoid random failures when
undefined symbols are referenced - with RTLD_NOW, if the symbol can't
be resolved at dlopen(3c) time, the dlopen call will fail.

scott

luser droog

unread,
Aug 1, 2013, 9:09:00 PM8/1/13
to
On Thursday, August 1, 2013 9:55:54 AM UTC-5, Scott Lurndal wrote:
> luser droog <mij...@yahoo.com> writes:
>
> >[reposted from comp.lang.c]
>
> >
>
> >I've avoided learning about dynamic loading for a long time, but
> >found that I need to learn to do it for things like multiple output
> >devices and running the whole program as a library call instead of
> >standalone. (This is all in the context of my Postscript Interpreter,
> >code.google.com/p/xpost)
> >
> >So I read http://en.wikipedia.org/wiki/Dynamic_loading
> >and followed a link to a Linux page, and whipped up this little
> >example/test. And it works!
> >
> >But it can't be that easy, right? I've got to be missing something.
> >
> >Please comment on the following code. (Ignore for the moment
> >the lack of error handling in the stack implementation; I'm mostly
> >interested in the interfacing.)
>
>
>
> From the interface standpoint, I generally use a common base class and
> specialize it in each shared object. For example, I have a hardware
> simulator that needs to load a variable set of device models. Each
> device model is implemented as a specialization of "class c_dlp",
> and a line printer model could be "class c_buffered_printer_dlp: public c_dlp".
>
>
> Then, each shared object (e.g. libdlp_buffered_printer.so) has an
> extern "C" get_dlp function (extern "C" so the name provided to
> dlsym(3c) isn't mangled).
>

Thanks for this. It's a lot to think through. I think I can
apply something similar to my output devices. I could have
a "base class" that implements the whole rendering chain
in software (map -> reduce -> clip -> paint). And then
different devices can override with their specific
functionality. Eg. a pdf- or svg- device would override
the paint method, receiving a mapped/reduced/clipped
poly-polygon. But a ppm- device would use the output from
the "base" paint method.

<snip>
> I recommend RTLD_NOW instead of RTLD_LAZY to avoid random failures when
> undefined symbols are referenced - with RTLD_NOW, if the symbol can't
> be resolved at dlopen(3c) time, the dlopen call will fail.
>

That does sound better. Wilco.

luser droog

unread,
Aug 1, 2013, 10:43:52 PM8/1/13
to
For multitasking, I'm following the existing designs
(what I can discern of them, that is) of Display Postscript
and NeWS, both of which normally operate in a cooperative mode.

So I plan to simulate multiple threads in a single OS-thread.

Here's a very simple mock-up of how I imagine this to work.
Functions (tasks) are not pre-empted during execution, so each thread
can consider longer sequences of execution to be atomic.


#include <stdio.h>
#include <setjmp.h>

jmp_buf dispatch;
int ntasks;
void (*task[10])(void);
int quit;

void yield(void) {
longjmp(dispatch, 1);
}

void loop() {
static int i = 0;
if(setjmp(dispatch))
i = (i+1) % ntasks;
while(!quit)
task[i]();
}

int acc = 0;

void a(void) {
if (acc > 10) quit = 1;
printf("A\n");
yield();
}
void b(void) {
acc *= 2;
printf("B\n");
yield();
}
void c(void) {
acc += 1;
printf("C\n");
yield();
}

int main() {
quit = 0;
ntasks = 3;
task[0] = a;
task[1] = b;
task[2] = c;
loop();
return 0;
}


In the real implementation, there are an appropriate set of synchronization
primitives (== control structures) including condition, monitor, fork, detach,
join. I'm still learning this part, but these lower level structures allow me
to make assumptions about the execution that would of course be hard to
guarantee using "normal" multi-threading.

luser droog

unread,
Aug 1, 2013, 11:13:37 PM8/1/13
to
On Thursday, August 1, 2013 9:43:52 PM UTC-5, luser droog wrote:
>
> So I plan to simulate multiple threads in a single OS-thread.
>
>
>
> Here's a very simple mock-up of how I imagine this to work.
>
> Functions (tasks) are not pre-empted during execution, so each thread
>
> can consider longer sequences of execution to be atomic.
>
>
>

Here's a more filled-out sketch of the same setjmp/longjmp
multitasking in an interpreter framework.

All functions pass-in a context structure, so there are
virtually no global variables (excepting the jmpbuf, and
string vectors corresponding to type and error enums).


vm.c:


#define _BSD_SOURCE
#define _GNU_SOURCE
#include <setjmp.h>
#include <stdbool.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>


#include "ob.h"

#define GLOBAL_FILE "global.img"

inline byte type(OBJ q) { return q.tag & typemask; }

/* constructors */

OBJ null = { .tag = nulltype };

OBJ mark = { .tag = marktype };

OBJ consbool (bool b) {
OBJ o = { .tag = booleantype };
o.BOOL.val = b;
return o;
}

OBJ consint (integer i) {
OBJ o = { .tag = integertype };
o.INT.val = i;
return o;
}

OBJ consreal (real r) {
OBJ o = { .tag = realtype };
o.REAL.val = r;
return o;
}


/* virtual memory space */

typedef struct memspace {
byte *base;
ulong used, max;
} memspace;

int pagesize /*= getpagesize()*/;

bool meminit (memspace *spc, int fd) {
spc->base = mmap( NULL,
spc->max = pagesize,
PROT_READ|PROT_WRITE,
fd!=-1?
MAP_SHARED:
MAP_SHARED|MAP_ANONYMOUS,
fd, 0 );
spc->used = 0;
return true;
}

addr memalloc (memspace *spc, ulong n) {
addr a;
if (spc->max - spc->used > n) {
int newmax = spc->max + (n/pagesize + 1)*pagesize;
spc->base = mremap(spc->base, spc->max, newmax, MREMAP_MAYMOVE);
spc->max = newmax;
}
a = spc->used;
spc->used += n;
return a;
}


/* machine state */

typedef struct mach {
memspace *vm[2]; /* 0-global, 1-local */
} mach;

bool machinit(mach *ma, memspace *glo) {
ma->vm[0] = glo;
ma->vm[1] = malloc(sizeof*ma->vm[1]);
return /*meminit(ma->vm, -1)
&& */meminit(ma->vm[1], -1);
}

bool init(mach *mp, int n) {
pagesize = getpagesize();

memspace *glo;
int memfil;
memfil = open( GLOBAL_FILE , O_RDWR);
if (memfil == -1) {
memfil = open( GLOBAL_FILE , O_CREAT|O_RDWR, 777);
}
glo = malloc(sizeof*glo);
meminit(glo, memfil);

int i;
for (i=0; i<n; i++)
machinit(mp+i, glo);
return true;
}

/* if LOCAL_BIT is set in an addr,
the address refers to local vm (ma->vm[1]) */
enum { LOCAL_BIT = 0x80000000 };

addr newbuf (mach *ma, int local, ulong n) {
addr a;
memspace *spc = ma->vm[local];
a = memalloc(spc, n);
if (local) a |= LOCAL_BIT;
return a;
}

inline byte *paddr(mach *ma, addr a) {
int local = !!(a & LOCAL_BIT);
return ma->vm[local]->base + (a & ~LOCAL_BIT);
}

addr newblock (mach *ma, ulong n) {
//add to delete list for currentsavelevel
}


inline OBJ srceval (mach *ma, OBJ q) {
//token
return q;
}
inline OBJ nameval (mach *ma, OBJ q) {
//load
return q;
}
inline OBJ arreval (mach *ma, OBJ q) {
//getinterval pushe
//get
return q;
}
inline OBJ opreval (mach *ma, OBJ q) {
//opexec
return q;
}
inline void liteval (mach *ma, OBJ q) {
//push
}

void eval (mach *ma) {
OBJ q /*= pope()*/;

if ( q.tag & exe ) switch (type(q)) {
default: break;

case filetype:
case stringtype: q = srceval(ma,q); return;

case nametype: q = nameval(ma,q); return;

case arraytype: q = arreval(ma,q); return;

case operatortype: q = opreval(ma,q); return;
}

liteval(ma,q); return;
}

jmp_buf taskswitch;
bool taskswitchvalid = false;

void run (mach *ma, int num_mach) {
static int i = 0;

if (setjmp(taskswitch))
i = (i+1) % num_mach;
taskswitchvalid = true;

while(!ma[i].quit)
eval(ma+i);

taskswitchvalid = false;
}


enum { NUM_MACHINES = 2 };
mach ines[NUM_MACHINES];


int main(int argc, char *argv[]) {
init(ines, NUM_MACHINES);
run(ines, NUM_MACHINES);
return 0;
}

/* eof */



ob.h:


typedef unsigned char byte;
typedef unsigned short word;
typedef unsigned long dword;
typedef long integ;
typedef float real;
typedef dword addr;

#define TYPES(_) \
_(invalid) \
_(null) \
_(mark) \
_(boolean) \
_(integer) \
_(real) \
_(string) \
_(name) \
_(array) \
_(dict) \
_(file) \
_(operator) \
_(packedarray) \
/* end TYPES */

#define AS_TYPE(X) X ## type ,

enum type { TYPES(AS_TYPE) };

enum tagmask {
exe = 0x8000,
Aread = 0x4000,
Awrite = 0x2000,
Aexec = 0x1000,
typemask = 0x00FF,
};

//#define type(o) ((o).tag & typemask)

typedef struct {
word tag;
} MARK;

typedef struct {
word tag;
byte val;
} BOOL;

typedef struct {
word tag;
integ val;
} INT;

typedef struct {
word tag;
real val;
} REAL;

typedef struct {
word tag;
word siz;
addr adr;
} STR;

typedef struct {
word tag;
word siz;
addr adr;
} ARR;

typedef struct {
word tag;
word siz;
addr adr;
} DIC;

typedef struct {
word tag;
word siz;
addr adr;
} FIL;

typedef struct {
word tag;
word opcode;
} OP;

typedef union {
byte tag;
INT INT;
MARK MARK;
BOOL BOOL;
REAL REAL;
STR STR;
ARR ARR;
DIC DIC;
FIL FIL;
OP OP;
} OBJ;



0 new messages