Combing Oberon-07 with C using Obc-3
By R. S. Doiel, 2021-06-14
This post explores integrating C code with an Oberon-07 module use Mike Spivey's Obc-3 Oberon Compiler. Last year I wrote a similar post for Karl Landström's OBNC. This goal of this post is to document how I created a version of Karl's Extension Library that would work with Mike's Obc-3 compiler. If you want to take a shortcut you can see the results on GitHub in my obc-3-libext repository.
From my time with OBNC I've come to rely on three modules from Karl's extension library. When trying to port some of my code to use with Mike's compiler. That's where I ran into a problem with that dependency. Karl's modules aren't available. I needed an extArgs, an extEnv andextConvert.
Mike's own modules that ship with Obc-3 cover allot of common ground with Karl's. They are organized differently. The trivial solution is to implement wrapping modules using Mike's modules for implementation. That takes case of extArgs and extEnv.
The module extConvert is in a another category. Mike's Conv module is
significantly minimalist. To solve that case I've create C code to perform
the needed tasks based on Karl's examples and used Mike's share library
compilation instructions to make it available inside his run time.
Background material
- Obc-3 website
- Installing Obc-3
- Adding primitives to Obc-3, this is how you extend Obc-3 with C
- Obc-3.1 Manual
- Obc-3 at GitHub
Differences: OBNC and Obc-3
The OBNC compiler written by Karl takes the approach of translating Oberon-07 code to C and then calling the C tool chain to convert that into a executable. Karl's compiler is largely written in C with some parts written in Oberon.
Mike's takes a different approach. His compiler uses a run time JIT and is written mostly in OCaml with some C parts and shell scripting. When you compile an Oberon program (either Oberon-2 or Oberon-07) using Mike's compiler you get a bunch of "*.k" files that the object code for Mike's thunder virtual machine and JIT. This can in turn be used to create a executable.
For implementing Oberon procedures in C Karl's expects an empty procedure body. e.g.
PROCEDURE DoMyThing();
BEGIN
END DoMyThing;
While Mike has added a "IS" phrase to the procedure signature to indicate what the C implementation is known as. There is no procedure body in Mike's implementation and the parameters need to map directly into a C data type.
PROCEDURE DoMyThing() IS "do_my_thing";
Of course both compilers have completely different command line options
and when you're integrating C shared libraries in Mike's you need to
call your local CC (e.g. GCC, clang) to create a share library file.
Mike has extended Oberon-07 SYSTEM to include SYSTEM.LOADLIB() which
takes a string containing the path to the compiler shared library.
In Karl's own Oberon-07 modules he uses the .obn file extension but
also accepts .Mod. In Mike's he uses .m and also accepts .Mod.
In this article I will be using .m as that simplified the recipe
of building and integrating the shared C libraries.
Similarities of OBNC and Obc-3
Both compilers provide for compiling Oberon-07 code, Mike's requires
the -07 option to be used to switch from Oberon-2. Both offer the
ability to extend reach into the host POSIX system by wrapping
C shared libraries. Both run on a wide variety of POSIX systems and
you can read the source code at your leisure. This last bit is
important.
Args, extArgs and extEnv.
Mike provides two features in his Args module. The first is access
to the command line arguments of the compiled program. The
second feature is to provide access to the host environment variables.
In Karl's implementation he separates Mikes Args.GetEvn() into
a module called extEnv. Here's Mike's module definition looks like ---
DEFINITION Args;
VAR argc* : INTEGER; (* this is equavilent to extArgs.count *)
PROCEDURE GetArg*(n: INTEGER; VAR s: ARRAY OF CHAR);
PROCEDURE GetEnv*(name: ARRAY OF CHAR; VAR s: ARRAY OF CHAR);
END Args.
My implementation of Karl's extArgs needs to look like ---
DEFINITION extArgs;
VAR count*: INTEGER; (* this is the same as Args.argc *)
PROCEDURE Get*(n: INTEGER; VAR arg: ARRAY OF CHAR; VAR res: INTEGER);
END extArgs.
This leaves us with a very simple module mimicking Karl's.
MODULE extArgs;
IMPORT Args;
VAR
count*: INTEGER;
PROCEDURE Get*(n: INTEGER; VAR arg: ARRAY OF CHAR; VAR res: INTEGER);
BEGIN
Args.GetArg(n + 1, arg); res := 0;
END Get;
BEGIN
count := Args.argc - 1;
END extArgs.
NOTE: In Mike's approach the zero-th arg is the program name.
In Karl's the zero-th arg is the first argument after the program
name. To get Karl's behavior with Mike's GetArg() I need to
adjust the offsets.
So far so good. How about implementing Karl's extEnv?
We've already seen Mike's Args so he doesn't have a matching
definition. Karl's extEnv looks like
DEFINITION extEnv;
PROCEDURE Get*(name: ARRAY OF CHAR; VAR value: ARRAY OF CHAR; VAR res: INTEGER);
END extEnv.
And again a simple mapping of features and you have
MODULE extEnv;
IMPORT Args, Strings;
PROCEDURE Get*(name : ARRAY OF CHAR; VAR value : ARRAY OF CHAR; VAR res : INTEGER);
VAR i, l1, l2 : INTEGER; val : ARRAY 512 OF CHAR;
BEGIN
l1 := LEN(value) - 1; (* Allow for trailing 0X *)
Args.GetEnv(name, val);
l2 := Strings.Length(val);
IF l2 <= l1 THEN
res := 0;
ELSE
res := l2 - l1;
END;
i := 0;
WHILE (i < l2) & (val[i] # 0X) DO
value[i] := val[i];
INC(i);
END;
value[i] := 0X;
END Get;
END extEnv.
extConvert requires more work
Mike provides a module called Conv.m for converting numbers
to strings. It is a little minimal for my current purpose.
That is easy enough to solve as Mike, like Karl provides a means
of extending Oberon code with C. That means I need to writeextConvert as both extConvert.m (the Oberon-07 part) andextConvert.c (the C part).
Here's Karl's definition
DEFINITION extConvert;
PROCEDURE IntToString*(i: INTEGER; VAR s: ARRAY OF CHAR; VAR done: BOOLEAN);
PROCEDURE RealToString*(x: REAL; VAR s: ARRAY OF CHAR; VAR done: BOOLEAN);
PROCEDURE StringToInt*(s: ARRAY OF CHAR; VAR i: INTEGER; VAR done: BOOLEAN);
PROCEDURE StringToReal*(s: ARRAY OF CHAR; VAR x: REAL; VAR done: BOOLEAN);
END extConvert.
I have implement my extConvert as a hybrid of Oberon-07 and calls
to a C shared library I will create called extConvert.c.
The Oberon file (i.e. extConvert.m)
MODULE extConvert;
IMPORT SYSTEM;
PROCEDURE IntToString*(i: INTEGER; VAR s: ARRAY OF CHAR; VAR done: BOOLEAN);
VAR l : INTEGER;
BEGIN
l := LEN(s); done := TRUE;
IntToString0(i, s, l);
END IntToString;
PROCEDURE IntToString0(i : INTEGER; VAR s : ARRAY OF CHAR; l : INTEGER) IS "conv_int_to_string";
PROCEDURE RealToString*(x: REAL; VAR s: ARRAY OF CHAR; VAR done: BOOLEAN);
VAR l : INTEGER;
BEGIN
l := LEN(s);
RealToString0(x, s, l);
END RealToString;
PROCEDURE RealToString0(x: REAL; VAR s: ARRAY OF CHAR; l : INTEGER) IS "conv_real_to_string";
PROCEDURE StringToInt*(s: ARRAY OF CHAR; VAR i: INTEGER; VAR done: BOOLEAN);
BEGIN
done := TRUE;
StringToInt0(s, i);
END StringToInt;
PROCEDURE StringToInt0(s : ARRAY OF CHAR; VAR i : INTEGER) IS "conv_string_to_int";
PROCEDURE StringToReal*(s: ARRAY OF CHAR; VAR x: REAL; VAR done: BOOLEAN);
BEGIN
done := TRUE;
StringToReal0(s, x);
END StringToReal;
PROCEDURE StringToReal0(s: ARRAY OF CHAR; VAR x : REAL) IS "conv_string_to_real";
BEGIN
SYSTEM.LOADLIB("./extConvert.so");
END extConvert.
If you review Mike's module code you'll see I have followed a similar pattern. Before calling out to C I take care of what house keeping I can in Oberon, then I call a "0" version of the function implemented in C. The C implementation are not exported only the wrapping Oberon procedures are.
Notice how the initialization block calls SYSTEM.LOADLIB("./extConvert.so"); this loads the C shared library so that the Oberon module can call out it it.
The C code in extConvert.c looks very traditional without the macros
you'd see in OBNC's implementation. Here's what the C code look like.
#include <stdlib.h>
#include <stdio.h>
void conv_int_to_string(int i, char *s, int l) {
snprintf(s, l, "%d", i);
}
void conv_real_to_string(float r, char *s, int l) {
snprintf(s, l, "%f", r);
}
void conv_real_to_exp_string(float r, char *s, int l) {
snprintf(s, l, "%e", r);
}
void conv_string_to_int(char *s, int *i) {
*i = atoi(s);
}
void conv_string_to_real(char *s, float *r) {
*r = atof(s);
}
The dance to compile the module and C shared library is very different
between OBNC and Obc-3. With Obc-3 we compile and skip linking
the wrapping Oberon module extConvert.m. We compile using CC
our C shared library. We can then put it all together to test
everything out in ConvertTest.m.
obc -07 -c extConvert.m
gcc -fPIC -shared extConvert.c -o extConvert.so
Our test code program looks like.
MODULE ConvertTest;
IMPORT T := Tests, Convert := extConvert;
VAR ts : T.TestSet;
PROCEDURE TestIntConvs() : BOOLEAN;
VAR test, ok : BOOLEAN;
expectI, gotI : INTEGER;
expectS, gotS : ARRAY 128 OF CHAR;
BEGIN test := TRUE;
gotS[0] := 0X; gotI := 0;
expectI := 101;
expectS := "101";
Convert.StringToInt(expectS, gotI, ok);
T.ExpectedBool(TRUE, ok, "StringToInt('101', gotI, ok) true", test);
T.ExpectedInt(expectI, gotI, "StringToInt('101', gotI, ok)", test);
Convert.IntToString(expectI, gotS, ok);
T.ExpectedBool(TRUE, ok, "IntToString(101, gotS, ok) true", test);
T.ExpectedString(expectS, gotS, "IntToString(101, gotS, ok)", test);
RETURN test
END TestIntConvs;
PROCEDURE TestRealConvs() : BOOLEAN;
VAR test, ok : BOOLEAN;
expectR, gotR : REAL;
expectS, gotS : ARRAY 128 OF CHAR;
BEGIN test := TRUE;
gotR := 0.0; gotS[0] := 0X;
expectR := 3.1459;
expectS := "3.145900";
Convert.StringToReal(expectS, gotR, ok);
T.ExpectedBool(TRUE, ok, "StringToReal('3.1459', gotR, ok) true", test);
T.ExpectedReal(expectR, gotR, "StringToReal('3.1459', gotR, ok)", test);
Convert.RealToString(expectR, gotS, ok);
T.ExpectedBool(TRUE, ok, "RealToString(3.1459, gotS; ok) true", test);
T.ExpectedString(expectS, gotS, "RealToString(3.1459, gotS, ok)", test);
RETURN test
END TestRealConvs;
BEGIN
T.Init(ts, "extConvert");
T.Add(ts, TestIntConvs);
T.Add(ts, TestRealConvs);
ASSERT(T.Run(ts));
END ConvertTest.
We compile and run our test program use the following commands (NOTE: Using Obc-3 you list all the dependent modules to possibly be compiled one the command line along with your program module).
obc -07 -o converttest extConvert.m Tests.m ConvertTest.m
./converttest
Source code for these modules is available on GitHub atgithub.com/rsdoiel/obc-3-libest
Next & Previous
- Next Revisiting Files
- Previous Beyond Oakwood, Modules and Aliases