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 and extConvert.
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 write extConvert
as both extConvert.m
(the Oberon-07 part) and
extConvert.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) {
(s, l, "%d", i);
snprintf}
void conv_real_to_string(float r, char *s, int l) {
(s, l, "%f", r);
snprintf}
void conv_real_to_exp_string(float r, char *s, int l) {
(s, l, "%e", r);
snprintf}
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 at github.com/rsdoiel/obc-3-libest
Next & Previous
- Next Revisiting Files
- Previous Beyond Oakwood, Modules and Aliases