Multilanguage¶
Multilanguage programming¶
By necessity, this chapter is not solely Unicon focused. Development with multiple programming languages, across multiple paradigms can be highly rewarding when applied to non-trivial application development. This can be as basic as adding a scripting engine to a program, or far more sophisticated, with multiple modules integrated into a cohesive whole.
For example, Emacs, a text editor now over 30 years old, merged a C programming core with Lisp scripting. Not the first multilanguage environment, but a relatively famous, and highly successful one.
Unicon is well placed for general purpose programming
, the feature set
allowing for many problems to be solved with only Unicon source code. This is
especially true when programming in the small, utility programs and light
applications. One thing missing from base Unicon is end user scripting.
Extending a core Unicon program means writing new Unicon source and
recompiling the application. Emacs has shown that allowing the end user to
extend a program with scripts is a powerful attribute, providing a path to add
useful features that were not originally designed into an application.
Along with scripting, there are vast quantities of libraries written in other languages that may benefit a Unicon program, alleviating the need to write new code to solve an already solved problem.
Luckily, Unicon allows for extending programs with foreign language libraries and exposing some powerful scripting engines (with some small effort).
A small downside is that Unicon requires a specific interface protocol,[1] so there is usually a few lines of C language source involved. This extra effort is fairly boilerplate, and working samples already exist, making this a rather convenient inconvenience.
Unicon ships with multiple forms of C integration (loadfunc and callout to name two), and from there, all kinds of potential is exposed.
Multilanguage programming with Unicon will usually be a two or three language
mix, Unicon
, C
, and any other language in use for the modules at
hand. This is possible because almost every programming language in existence
provides access to an ABI, application binary interface, compatible
with platform specific C compilers. C becomes the crossroads of a many to
one to many integration hub. The required C layer is usually small in terms
of binary size and source code line count, and as mentioned earlier, there are
existing examples to ease the burden on a Unicon developer.
See S-Lang, COBOL integration, Javascripting and Mini Ruby for some example integrations.
[1] | (1, 2) The Unicon foreign function interface protocol is in place to manage the high level Unicon datatypes expectations. Unicon data types are held in encoded descriptors, and these need conversion to and from the native ABI. |
loadfunc¶
loadfunc is a handy piece of kit. It allows Unicon to load libraries and
then define Unicon functions that will invoke special C functions. The [1]
requires a small piece of C that accepts int argc, descpriptor argv[]
, or
a count of arguments and pointer to an array of Unicon descriptors.
Argument 0 is reserved for the return result, so argc
is always at least
1.
The descriptor
data structures are define in ipl/cfuncs/icall.h
and that header file must be included in the source that defines the callable
functions.
The descriptor
is what gives Unicon its variant data type powers. A data
item can be a string, a number, a list, a real, etcetera and the structure can
assigned to a variable (or passed to C functions). Variables do not have a
type, and can reference any data, determined by the descriptor. The
Programs section has many examples of using loadfunc.
The small wrappers, written in C, are then free to call any other C function
as a native call. Results from these functions can then be passed back to
Unicon by setting the argv[0]
element. There are macros to make this very
easy. Fail
, Return
, RetString
, RetInteger
, RetReal
, and
so on, all documented in ipl/cfuncs/icall.h.
There are also a helper macros for testing the Unicon type passed into the wrapper, and for converting the descriptor values to native C data types.
IconType
is used for testing. ArgString
will ensure (and convert if
necessary) a particular descriptor is a C char
pointer. StringVal
will return the char *
. Same for ArgInteger
, ArgReal
, ArgList
(and others) along with the corresponding IntegerVal
, RealVal
,
ListVal
, etcetera. Read icall.h for all current details.
C Native¶
Note
Superseded, see libffi below. But read through this section as it details a lot of important background. The libffi version uses a very similar Unicon programmer interface, but offers a lot more platforms from a much more mature and well tested codebase.
Here is an experimental enhancement to Unicon that allows directly calling
almost any C function, without need for a wrapper. This builds on loadfunc
and defines 2 (or 3) new loadable functions. addLibrary
will add a
dynamic shared object library to the loader search path. native
allows
for calling C functions directly, by string name and an enumerated constant to
determine the return type. Other arguments are tested by Unicon type and
passed to C having built a call frame, using inline assembler.
The listing below provides support for x86_64, System V ABI calling
conventions. Other platform specific assembler will be required to add
support for other systems. This is relatively straight forward inline
assembler, 17 instructions for the initial trials. It allows up to 6
arguments, mixed integer, real or pointer/handle argument types and handles
void returns along with integral (numbers/pointers), and double
floating
point data for use as Unicon Real numbers, along with String types.
The code needs to be loaded into Unicon space, using loadfunc.
addLibrary
allows Unicon to add libraries to the dynamic search path.
native
is then used to lookup the call vectors (by string name, similar to
loadfunc but then marshals the other arguments and sets up a valid Unicon
return descriptor. No other wrappers are required.
First the new functions:
/* A new Unicon C native FFI layer, with a little assembler thrown in */
#include <stdio.h>
#include <dlfcn.h>
#include "icall.h"
#include "natives.h"
static void *dlHandle;
int
addLibrary(int argc, descriptor argv[])
{
//union block dlBlock;
//descriptor dlBlock;
/* Add a new library to the dynamic search path */
ArgString(1)
if (!dlHandle) {
dlHandle = dlopen(StringVal(argv[1]), RTLD_LAZY | RTLD_GLOBAL);
if (!dlHandle) {
fprintf(stderr, "dlHandle error\n");
fflush(stderr);
Error(500);
}
}
//dlBlock = mkExternal(dlHandle, sizeof(dlHandle));
//RetExternal(dlBlock);
RetInteger((long)dlHandle);
}
//static void *(*func)();
static void (*func)();
union blob {
long lvalue;
double rvalue;
float fvalue;
char *svalue;
};
static union blob fromc;
static union blob fromf;
/* Let the good times roll */
int
native(int argc, descriptor argv[])
{
/* a dlsym function pointer */
char *dlMsg;
/* Integers and pointers go in RDI, RSI, RDX, RCX/R10, R8, R9 */
/* Floating point in XMM0 - XMM6 */
/* They are two seperate sets */
union blob ipregs[7];
union blob fregs[7];
long retType;
char inType;
int ips = 1; /* count of integer/pointer args, reserve 0 */
int rs = 1; /* count of floating args, reserved 0, might not use */
/* first the function name */
ArgString(1);
/* second is return type, encoded in natives.inc matched in natives.h */
ArgInteger(2);
retType = IntegerVal(argv[2]);
/* Load the function pointer */
dlerror();
*(void **)(&func) = dlsym(dlHandle, StringVal(argv[1]));
dlMsg = dlerror();
if (dlMsg) {
fprintf(stderr, "dlsym fail: %s\n", dlMsg);
fflush(stderr);
Error(500);
}
if (!func) Fail;
/* marshalling by assembly to set up the variant call frames */
for (int argi = 3; argi <= argc; argi++) {
inType = IconType(argv[argi]);
/* Integers and pointers go in RDI, RSI, RDX, RCX/R10, R8, R9 */
/* Floating point in XMM0 - XMM6 */
/* They are two seperate sets */
switch(inType) {
case 'i':
ArgInteger(argi);
ipregs[ips++].lvalue = IntegerVal(argv[argi]);
break;
case 'r':
ArgReal(argi);
fregs[rs++].rvalue = RealVal(argv[argi]);
break;
case 's':
ArgString(argi);
ipregs[ips++].svalue = StringVal(argv[argi]);
break;
}
}
/* Function vector in r11 */
asm("movq func(%%rip), %%r11;"
: /* no output operand */
: /* no input operand */
:"%r11"
);
/* Take values from ipregs ints and fregs floats */
asm("movq %0, %%r9;"
: /* no output operand */
:"r"(ipregs[6])
:"%r9" /* clobber */
);
asm("movq %0, %%r8;"
:
:"r"(ipregs[5])
:"%r8"
);
asm("movq %0, %%rcx;"
:
:"r"(ipregs[4])
:"%rcx"
);
asm("movq %0, %%r10;" /* For Linux kernel calls instead of RCX */
:
:"r"(ipregs[4])
:"%r10"
);
asm("movq %0, %%rdx;"
:
:"r"(ipregs[3])
:"%rdx"
);
asm("movq %0, %%rsi;"
:
:"r"(ipregs[2])
:"rsi"
);
asm("movq %0, %%rdi;"
:
:"r"(ipregs[1])
:"%rdi"
);
asm("movsd %0, %%xmm5;"
:
:"m"(fregs[6])
:"xmm5"
);
asm("movsd %0, %%xmm4;"
:
:"m"(fregs[5])
:"xmm4"
);
asm("movsd %0, %%xmm3;"
:
:"m"(fregs[4])
:"xmm3"
);
asm("movsd %0, %%xmm2;"
:
:"m"(fregs[3])
:"xmm2"
);
asm("movsd %0, %%xmm1;"
:
:"m"(fregs[2])
:"xmm1"
);
asm("movsd %0, %%xmm0;"
:
:"m"(fregs[1])
:"xmm0"
);
asm("call *%r11");
asm("movsd %xmm0, fromf(%rip)");
asm("movq %rax, fromc(%rip)");
/* Return type, as specfied in argument 2 enum */
switch(retType) {
case TYPEVOID:
Return;
break;
case TYPESTAR:
RetInteger(fromc.lvalue);
break;
case TYPEINT:
RetInteger((long)fromc.lvalue);
break;
case TYPEFLOAT:
RetReal((float)fromf.rvalue);
break;
case TYPEDOUBLE:
RetReal((double)fromf.rvalue);
break;
case TYPESTRING:
RetString(fromc.svalue);
break;
default:
RetInteger((long)fromc.lvalue);
break;
}
}
/* Delta between managing float versus double */
/* Requires some refactoring to merge with above */
int
nativeFloat(int argc, descriptor argv[])
{
/* a dlsym function pointer */
char *dlMsg;
/* Integers and pointers go in RDI, RSI, RDX, RCX/R10, R8, R9 */
/* Floating point in XMM0 - XMM6 */
/* They are two seperate sets here, duplicate work, more coverage */
union blob ipregs[7];
union blob fregs[7];
long retType;
char inType;
int ips = 1; /* count of integer/pointer args, reserve 0 */
int rs = 1; /* count of floating args, reserved 0, might not use */
/* first the function name */
ArgString(1);
/* second is return type, encoded in natives.inc */
ArgInteger(2);
retType = IntegerVal(argv[2]);
/* load the function pointer */
dlerror();
*(void **)(&func) = dlsym(dlHandle, StringVal(argv[1]));
dlMsg = dlerror();
if (dlMsg) {
fprintf(stderr, "dlsym fail: %s\n", dlMsg);
fflush(stderr);
Error(500);
}
if (!func) Fail;
/* marshalling by assembly to set up the variant call frames */
for (int argi = 3; argi <= argc; argi++) {
inType = IconType(argv[argi]);
/* Integers and pointers go in RDI, RSI, RDX, RCX/R10, R8, R9 */
/* Floating point in XMM0 - XMM6 */
/* They are two seperate sets */
switch(inType) {
case 'i':
ArgInteger(argi);
ipregs[ips++].lvalue = IntegerVal(argv[argi]);
break;
case 'r':
ArgReal(argi);
fregs[rs++].fvalue = (float)RealVal(argv[argi]);
break;
case 's':
ArgString(argi);
ipregs[ips++].svalue = StringVal(argv[argi]);
break;
}
}
/* Function vector in r11 */
asm("movq func(%%rip), %%r11;"
: /* no output operand */
: /* no input operand */
:"%r11"
);
/* Take values from ipregs ints and fregs floats */
asm("movq %0, %%r9;"
: /* no output operand */
:"r"(ipregs[6])
:"%r9" /* clobber */
);
asm("movq %0, %%r8;"
:
:"r"(ipregs[5])
:"%r8"
);
asm("movq %0, %%rcx;"
:
:"r"(ipregs[4])
:"%rcx"
);
asm("movq %0, %%r10;" /* For Linux kernel calls instead of RCX */
:
:"r"(ipregs[4])
:"%r10"
);
asm("movq %0, %%rdx;"
:
:"r"(ipregs[3])
:"%rdx"
);
asm("movq %0, %%rsi;"
:
:"r"(ipregs[2])
:"rsi"
);
asm("movq %0, %%rdi;"
:
:"r"(ipregs[1])
:"%rdi"
);
asm("movss %0, %%xmm5;"
:
:"m"(fregs[6])
:"xmm5"
);
asm("movss %0, %%xmm4;"
:
:"m"(fregs[5])
:"xmm4"
);
asm("movss %0, %%xmm3;"
:
:"m"(fregs[4])
:"xmm3"
);
asm("movss %0, %%xmm2;"
:
:"m"(fregs[3])
:"xmm2"
);
asm("movss %0, %%xmm1;"
:
:"m"(fregs[2])
:"xmm1"
);
asm("movss %0, %%xmm0;"
:
:"m"(fregs[1])
:"xmm0"
);
asm("call *%r11");
asm("movss %xmm0, fromf(%rip)");
asm("movq %rax, fromc(%rip)");
/* Return type, as specfied in argument 2 enum */
switch(retType) {
case TYPEVOID:
Return;
break;
case TYPESTAR:
RetInteger(fromc.lvalue);
break;
case TYPEINT:
RetInteger((long)fromc.lvalue);
break;
case TYPEFLOAT:
RetReal((float)fromf.fvalue);
break;
case TYPEDOUBLE:
RetReal((double)fromf.fvalue);
break;
case TYPESTRING:
RetString(fromc.svalue);
break;
default:
RetInteger((long)fromc.lvalue);
break;
}
}
Before the sample run, here are the two small support files for enumerating the encoded return types, and other related paperwork. These two files must be kept in synch.
A Unicon $include
file:
#
# native.inc, Native type constants
#
$define TYPEVOID 0
$define TYPESTAR 1
$define TYPECHAR 2
$define TYPESHORT 3
$define TYPEINT 4
$define TYPEFLOAT 5
$define TYPEDOUBLE 6
$define TYPESTRING 7
(Note the filename, .inc
), it is a Unicon preprocessor include file.
A small C #include
header.
/* Native return types, must match natives.icn from Unicon */
#define TYPEVOID 0
#define TYPESTAR 1
#define TYPECHAR 2
#define TYPESHORT 3
#define TYPEINT 4
#define TYPEFLOAT 5
#define TYPEDOUBLE 6
#define TYPESTRING 7
Now a test head of small C functions to try out various call frames.
/* testnative.c, testing an experimental Unicon native call layer */
#include <stdio.h>
int
testnative(int one, int two)
{
int ivalue;
fprintf(stderr, "In testnative %d %d\n", one, two);
fflush(stderr);
ivalue = one + two;
return ivalue;
}
double
testdouble(double one, double two)
{
fprintf(stderr, "In testdouble %f %f\n", one, two);
fflush(stderr);
return one + two;
}
double
testfive(double one, double two, int three, double four, double five)
{
double sum;
fprintf(stderr, "In testfive %f %f %d %f %f\n", one, two, three,
four, five);
fflush(stderr);
sum = one + two + three + four + five;
return sum;
}
void *
teststar()
{
fprintf(stderr, "In teststar\n");
fflush(stderr);
return &teststar;
}
char *
teststring(char *echo)
{
fprintf(stderr, "In teststring with #%s#\n", echo);
fflush(stderr);
return echo;
}
char *
testmulti(char *echo, int i, double d)
{
fprintf(stderr, "In testmulti with #%s# %d %f\n", echo, i, d);
fflush(stderr);
return echo;
}
double
testmultid(char *echo, int i, double d)
{
fprintf(stderr, "In testmultid with #%s# %d %f\n", echo, i, d);
fflush(stderr);
return d;
}
/* Requires using nativeFloat from Unicon */
float
testfloat(float one, float two)
{
fprintf(stderr, "In testfloat %f %f\n", one, two);
fflush(stderr);
return one + two;
}
float
testmultif(char *echo, int i, float f)
{
fprintf(stderr, "In testmultif with #%s# %d %f\n", echo, i, f);
fflush(stderr);
return f;
}
Unicon code to put in all in motion:
#
# testffi.icn, demonstrate an experimental C FFI
#
$include "natives.inc"
procedure main()
# will be RTLD_LAZY | RTLD_GLOBAL (so add to the search path)
addLibrary := loadfunc("./uninative.so", "addLibrary")
# allow arbitrary C functions, marshalled by a piece of assembler
native := loadfunc("./uninative.so", "native")
# add the testing functions to the dlsym search path,
# the handle is somewhat irrelevant, but won't be soonish
dlHandle := addLibrary("./libtestnative.so")
write("Unicon dlHandle: ", dlHandle)
write()
# pass two integers, get the sum as int
ans := native("testnative", TYPEINT, 40, 2)
write("Unicon: called testnative and got ", ans)
if ans ~= 42 then write("ERROR with testnative")
write()
# pass two reals, get the sum as real
ans := native("testdouble", TYPEDOUBLE, 9.0, 8.0)
write("Unicon: called testdouble and got ", ans)
if ans ~= 17.0 then write("ERROR with testdouble")
write()
# third arg is an integer, returns real
ans := native("testfive", TYPEDOUBLE, 1.0, 2.0, 3, 4.0, 5.0)
write("Unicon: called testfive and got ", ans)
if ans ~= 15.0 then write("ERROR with testfive")
write()
# get a pointer/handle
ans := native("teststar", TYPESTAR)
write("Unicon: called teststar and got ", ans)
if ans = 0 then write("ERROR with teststar")
write()
# a string
ans := native("teststring", TYPESTRING, "this is a string")
write("Unicon: called teststring and got ", ans)
if ans ~== "this is a string" then write("ERROR with teststring")
write()
# a string, and int and a real, returning string
ans := native("testmulti", TYPESTRING,
"this is a string for multi", 42, &pi)
write("Unicon: called testmulti and got ", ans)
if ans ~== "this is a string for multi" then
write("ERROR with testmulti")
write()
# a string, and int and a real, returning real
ans := native("testmultid", TYPEDOUBLE,
"this is a string for multid", 42, &pi)
write("Unicon: called testmultid and got ", ans)
if ans ~= &pi then write("ERROR with testmultid")
write()
#
# Variant for float versus double
#
# pass two reals, get the sum as real
write("float variant")
nativeFloat := loadfunc("./uninative.so", "nativeFloat")
ans := nativeFloat("testfloat", TYPEFLOAT, 9.0, 8.0)
write("Unicon: called testfloat and got ", ans)
if ans ~= 17.0 then write("ERROR with testfloat")
write()
# a string, and int and a real, returning real
ans := nativeFloat("testmultif", TYPEFLOAT,
"this is a short string for multif", 21, &pi/2)
write("Unicon: called testmultif and got ", ans)
if ans - &pi/2 > 0.0000001 then write("ERROR with testmultif")
write()
end
Two commands to prep the new support function and build the test heads
prompt$ gcc -o uninative.so -shared -fPIC native.c
prompt$ gcc -o libtestnative.so -shared -fPIC testnative.c
And finally, the sample run:
prompt$ unicon -s testffi.icn -x
Unicon dlHandle: 22708080
In testnative 40 2
Unicon: called testnative and got 42
In testdouble 9.000000 8.000000
Unicon: called testdouble and got 17.0
In testfive 1.000000 2.000000 3 4.000000 5.000000
Unicon: called testfive and got 15.0
In teststar
Unicon: called teststar and got 140061578336819
In teststring with #this is a string#
Unicon: called teststring and got this is a string
In testmulti with #this is a string for multi# 42 3.141593
Unicon: called testmulti and got this is a string for multi
In testmultid with #this is a string for multid# 42 3.141593
Unicon: called testmultid and got 3.141592653589793
float variant
In testfloat 9.000000 8.000000
Unicon: called testfloat and got 17.0
In testmultif with #this is a short string for multif# 21 1.570796
Unicon: called testmultif and got 1.570796370506287
Woohoo, swing you partner round and round. Calling C without any wrapper functions.
libharu¶
This makes things a little easier when it comes to some of the more feature
rich libraries available, first trial is libharu
a C library for creating
PDF documents.
#
# haru.icn, demonstrate a new C FFI (first fail, double versus float)
#
$include "natives.inc"
$define HPDF_COMP_ALL 15
$define HPDF_PAGE_MODE_USE_OUTLINE 1
$define HPDF_PAGE_SIZE_LETTER 0
$define HPDF_PAGE_PORTRAIT 0
procedure main()
local dlHandle, pdf, page1, rc, savefile := "harutest.pdf"
# will be RTLD_LAZY | RTLD_GLOBAL (so add to the search path)
addLibrary := loadfunc("./uninative.so", "addLibrary")
# allow arbitrary C functions, marshalled by a piece of assembler
# assume float instead of double, changes the inline assembler
# movsd versus movdd
native := loadfunc("./uninative.so", "nativeFloat")
# add libhpdf to the dlsym search path, the handle is irrelevant
dlHandle := addLibrary("libhpdf.so")
pdf := native("HPDF_New", TYPESTAR, 0, 0)
rc := native("HPDF_SetCompressionMode", TYPEINT, pdf, HPDF_COMP_ALL)
rc := native("HPDF_SetPageMode", TYPEINT, pdf,
HPDF_PAGE_MODE_USE_OUTLINE)
$ifdef PROTECTED
rc := native("HPDF_SetPassword", TYPEINT, pdf, "owner", "user")
savefile := "harutest-pass.pdf"
$endif
page1 := native("HPDF_AddPage", TYPESTAR, pdf)
rc := native("HPDF_Page_SetHeight", TYPEINT, page1, 220.0)
rc := native("HPDF_Page_SetWidth", TYPEINT, page1, 200.0);
#/* A part of libharu pie chart sample, Red*/
rc := native("HPDF_Page_SetRGBFill", TYPEINT, page1, 1.0, 0.0, 0.0);
rc := native("HPDF_Page_MoveTo", TYPEINT, page1, 100.0, 100.0);
rc := native("HPDF_Page_LineTo", TYPEINT, page1, 100.0, 180.0);
rc := native("HPDF_Page_Arc", TYPEINT, page1, 100.0, 100.0, 80.0,
0.0, 360 * 0.45);
#pos := native("HPDF_Page_GetCurrentPos (page);
rc := native("HPDF_Page_LineTo", TYPEINT, page1, 100.0, 100.0);
rc := native("HPDF_Page_Fill", TYPEINT, page1);
rc := native("HPDF_SaveToFile", TYPEINT, pdf, savefile)
native("HPDF_Free", TYPEVOID, pdf)
end
And then generating a password protected sample PDF.
prompt$ unicon -s -DPROTECTED haru-v1.icn -x
In a less short example, the password PROTECTED option would be tested at runtime, not at compile time, along with other control options for things like supported page sizes, compression and default character encodings.
prompt$ ls -l harutest-pass.pdf
-rw-rw-r-- 1 btiffin btiffin 1113 Oct 27 04:47 harutest-pass.pdf
GnuCOBOL¶
Calling COBOL modules is now a breeze.
*>
*> Demonstrate Unicon native call of COBOL modules
*>
identification division.
program-id. cobolnative.
data division.
working-storage section.
linkage section.
01 one usage binary-long.
01 two usage binary-long.
procedure division using by value one two.
display "GnuCOBOL got " one ", " two
compute return-code = one + two
goback.
end program cobolnative.
programs/uniffi/cobolnative.cob
A caller:
#
# testcob.icn, test calling COBOL without wrapper
#
$include "natives.inc"
procedure main()
# will be RTLD_LAZY | RTLD_GLOBAL (so add to the search path)
addLibrary := loadfunc("./uninative.so", "addLibrary")
# allow arbitrary C functions, marshalled by a small piece of assembler
native := loadfunc("./uninative.so", "native")
# add the testing functions to the dlsym search path,
# the handle is somewhat irrelevant, but won't be soonish
dlHandle := addLibrary("./cobolnative.so")
# initialize GnuCOBOL
native("cob_init", TYPEVOID)
# pass two integers, get back a sum
ans := native("cobolnative", TYPEINT, 40, 2)
write("Unicon: called sample and got ", ans)
# rundown the libcob runtime
native("cob_tidy", TYPEVOID)
end
A build, and a run:
prompt$ cobc -m cobolnative.cob -Wno-unfinished
prompt$ unicon -s testcob.icn -x
GnuCOBOL got +0000000040, +0000000002
Unicon: called sample and got 42
And there is Unicon calling COBOL, with no additional wrappers, besides the
native
functions. Data passed, and results returned to Unicon.
Supported platforms¶
Currently, the assembler required to make this work is x86_64 GNU/Linux only. That will change if/when people show interest.
Note
libffi fixes that support problem, lots of platforms supported. libffi supersedes, but does not displace, the above information.
libffi¶
After playing with the C Native interface, and actually looking into how
many platforms would require the assembly layer, bumped into libffi
.
libffi
is a library that does the call frame setup, very much like the
native interface described above. Except it is mature, well tested, and
already supports many tens of platforms and various compiler systems. So,
while the above interface works, and will be available, the libffi
system
is already ahead of the game, and will be put to use instead of hand rolled
assembler.
https://sourceware.org/libffi/
The user interface from a Unicon point of view, is almost identical to what is
shown above, but extended with a few more features, like support for
struct
, something that would have been a little trickier with hand rolled
assembly developed from scratch.
A new native(...)
function is defined in the same way with loadfunc, but
instead of including inline assembler, it uses ffi_prep_cif
and
ffi_call
.
- FFI
- Foreign Function Interface
- CIF
- Call InterFace
The only visible change is uninative
in now uniffi
, and -lffi
is
included for access to the libffi
features.
Using libffi
suffers the same problem of float
versus double
that
was in the previous native sequence. A Unicon Real is a double
and
sometimes C requires a float
. A new interface was developed to help
get around this problem; a datatype override can be included by using a List (arrays)
structure as part of any arguments passed on to C.
As an explanation:
The current marshalling layer pulls data from Unicon and tests the type of
data passed. Integer values are mapped to long
, Real values are mapped to
double
.
For example, the following code, just works:
ans := native("j0", TYPEDOUBLE, &pi)
That code will call the libm
Bessel function of the first kind, order 0,
which assumes a double
floating point input and returns a double
. The
Unicon type does not need any further conversion. A real is effectively a
double.
The problem comes when you want to call a lower precision float
version.
The first crack at a solution was to have native(...)
and
nativeFloat(...)
routines. The nativeFloa(...)
function assumed that
all real values were to be demoted to float
values. It worked, but it
meant that you could never mix float
and double
arguments. The new
sequence drops the nativeFloat
function and provides an override option to
the Unicon programmer.
The new experiment uses List (arrays) data for those times when an override is required.
# pass pi as a real (as double), get back a real (as double)
ans := native("j0", TYPEDOUBLE, &pi)
write("j0(pi) = ", ans)
# pass pi as a real (as float), get back a real (as float)
ans := naitive("j0f", TYPEFLOAT, [&pi, TYPEFLOAT])
write("j0f(pi) = ", ans)
# pass pi as a normal double, pass e as a float, get back a double
ans := native("mymath", TYPEDOUBLE, &pi, [&e, TYPEFLOAT])
The override allows freely mixed float
and double
arguments pulled
from the Unicon Real type.
An alternative was an overly burdensome type specifier required for every argument, which make the source code less friendly to write and a little bit harder to read with all the extra type specifiers getting in the way.
As a bonus, it also allows other hard to qualify options in Unicon, such as
# pass integer i (by address) get back an integer
ans := native("indirect", TYPEINT, [i, TYPESTAR])
write("indirect( ", i, ") = ", ans)
If the C function is defined as
int
indirect(int *i)
{
return *i * 2;
}
The Unicon code listing above will call the function by passing the address of a copy of the integer value. This is not quite the same as pass by reference, but is pass by content. The called routine gets a pointer (common in C library functions) but will not be able to change the source data.
This is a limitation, but it protects the immutable property of Unicon base data types. This limitation will stand. The type override opens the possibility of calling a C functions with pointers, without falling back to writing a wrapper (unless the routine actually needs to change the referenced data for proper functioning). At that point, to protect Unicon immutable data, an extra wrapper routine would need to be written, burden on the Unicon programmer to follow the loadfunc model of C integration. It’s not a huge burden really, but does require a little bit of C code.
There may be a future addition to the uniffi.c feature set to allow changing referenced data, by passing a list from Unicon. The caller would then be able to reassign the output values using normal Unicon assignment operators.
Note
There will never be a feature added that allows immutable Unicon data
to be changed in place with uniffi
. That goes too far against the
grain and the spirit of Unicon programming.
The current libffi
interface tests just as well as the hand rolled
assembler. To be honest it actually works better, many edge and corner cases
have been debugged in libffi, and the cross platform support is a complete
boon.
/*
A new Unicon C native FFI layer, with libffi
Tectonics: gcc -o uniffi.so -shared -fPIC uniffi.c -lffi
Phase 2 trial
todo refactor the argument scanner
work out ORing in the TYPESTAR arg override
*/
#include <stdio.h>
#include <ffi.h>
#include "icall.h"
#include "natives.h"
/* a dlopen handle */
static void *dlHandle;
/* a dlsym function pointer */
static void (*func)();
/* storage blob for arguments */
union blob {
long lvalue;
double rvalue;
float fvalue;
char *svalue;
};
#ifndef RTLD_LAZY /* normally from <dlfcn.h> */
#define RTLD_LAZY 1
#endif /* RTLD_LAZY */
#if NT
void *
dlopen(char *name, int flag)
{ /* LoadLibrary */
return (void *)LoadLibrary(name);
}
void *
dlsym(void *handle, char *sym)
{
return (void *)GetProcAddress((HMODULE)handle, sym);
}
int
dlclose(void *handle)
{ /* FreeLibrary */
return FreeLibrary((HMODULE)handle);
}
char *
dlerror(void)
{
return "undiagnosed dynamic load error";
}
#else /* NT */
#include <dlfcn.h>
#endif /* NT */
#ifdef FreeBSD
/*
If DL_GETERRNO exists, this is an FreeBSD 1.1.5 or 2.0
which lacks dlerror(); supply a substitute.
*/
#ifdef DL_GETERRNO
char *
dlerror(void)
{
int no;
if (0 == dlctl(NULL, DL_GETERRNO, &no))
return(strerror(no));
else
return(NULL);
}
#endif
#endif /* __FreeBSD__ */
int
ffi(int argc, descriptor argv[])
{
/* ffi_retval will point to an appropriate return slot */
void *ffi_retval;
/* ffi_rettype will point to the address of an ffi_type indicator */
void *ffi_rettype;
/* The cif setup requires the return type and value slot */
ffi_cif cif;
/* allow for 127 arguments, the C standard minimum limit */
ffi_type *args[127];
void *values[127];
/* a variant result block */
ffi_arg rc;
/* ffi library call result codes */
int ffi_stat;
/* function lookup name, and possible alternate */
char *funcname;
char *funcname2;
/* dlsym lookup error messages */
char *dlMsg;
/* local copy of args, probably dumped in phase 2 */
union blob ipregs[127];
union blob fregs[127];
/* the return type enum from Unicon, in natives.h */
long retType;
/* the base Unicon argument data type, may be overridden */
char inType;
/* pointed to by ffi_retval, for ffi_prep_cif */
long intSlot;
double doubleSlot;
void *pointerSlot;
float floatSlot;
/* current count stashed arguments */
int ips = 0;
/* first the function name */
ArgString(1);
/* second is return type, from natives.inc matched in natives.h */
ArgInteger(2);
retType = IntegerVal(argv[2]);
/* look up the function entry point */
funcname = StringVal(argv[1]);
dlerror();
*(void **)(&func) = dlsym(dlHandle, funcname);
dlMsg = dlerror();
if (dlMsg) {
fprintf(stderr, "dlsym fail: %s\n", dlMsg);
fflush(stderr);
Fail; //Error(500);
}
/* try alternative name with initial underscore */
if (!func) {
funcname2 = malloc(strlen(funcname + 2));
if (funcname2) {
*funcname2 = '_';
strcpy(funcname2 + 1, funcname);
*(void **)(&func) = dlsym(dlHandle, funcname2);
free(funcname2);
}
}
if (!func) Fail;
/* Return type, pass an indirect pointer to ffi_call */
switch(retType) {
case TYPEVOID:
ffi_rettype = &ffi_type_void;
ffi_retval = NULL;
break;
case TYPESTAR:
ffi_rettype = &ffi_type_pointer;
ffi_retval = &pointerSlot;
break;
case TYPEINT:
ffi_rettype = &ffi_type_slong;
ffi_retval = &intSlot;
break;
case TYPEFLOAT:
ffi_rettype = &ffi_type_float;
ffi_retval = &floatSlot;
break;
case TYPEDOUBLE:
ffi_rettype = &ffi_type_double;
ffi_retval = &doubleSlot;
break;
case TYPESTRING:
ffi_rettype = &ffi_type_pointer;
ffi_retval = &pointerSlot;
break;
default:
ffi_rettype = &ffi_type_slong;
ffi_retval = &intSlot;
break;
}
/* pull out types and values */
for (int argi = 3; argi <= argc; argi++) {
inType = IconType(argv[argi]);
/*
fprintf(stderr, "%d arg: %d, IconType: %d '%c'\n",
argc, argi, inType, inType);
fflush(stderr);
*/
int llen;
struct descrip slot[2];
int forceType;
switch(inType) {
/* Special case for lists, second value is forced datatype */
/* currently a cheater stub only handling double/float cases */
case 'L':
llen = ListLen(argv[argi]);
if (llen != 2) {
ipregs[ips].rvalue = 0.0;
ipregs[ips].fvalue = 0.0;
break;
}
/* from looking at IListVal and RListVal */
cpslots(&argv[argi], &slot[0], 1, 3);
forceType = IntegerVal(slot[1]);
switch(forceType) {
case TYPEFLOAT:
ipregs[ips].fvalue = (float)RealVal(slot[0]);
args[ips] = &ffi_type_float;
values[ips] = &ipregs[ips].fvalue;
ips++;
break;
case TYPEDOUBLE:
ipregs[ips].rvalue = RealVal(slot[0]);
fregs[ips].rvalue = RealVal(slot[0]);
args[ips] = &ffi_type_double;
values[ips] = &(fregs[ips].rvalue);
ips++;
break;
default:
fprintf(stderr, "forceType %d not yet supported\n",
forceType);
fflush(stderr);
break;
}
break;
case 'i':
ArgInteger(argi);
ipregs[ips].lvalue = IntegerVal(argv[argi]);
args[ips] = &ffi_type_slong;
values[ips] = &(ipregs[ips].lvalue);
ips++;
break;
case 'r':
ArgReal(argi);
fregs[ips].rvalue = RealVal(argv[argi]);
args[ips] = &ffi_type_double;
values[ips] = &(fregs[ips].rvalue);
ips++;
break;
case 's':
ArgString(argi);
ipregs[ips].svalue = StringVal(argv[argi]);
args[ips] = &ffi_type_pointer;
values[ips] = &(ipregs[ips].svalue);
ips++;
break;
}
}
/* Initialize the cif */
ffi_stat = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, argc - 2,
ffi_rettype, args);
if (ffi_stat == FFI_OK) {
/* the magic call */
ffi_call(&cif, *func, ffi_retval, values);
} else {
fprintf(stderr, "ffi_prep_cif failed: %d\n", ffi_stat);
fflush(stderr);
Fail;
}
/* Return type, as specfied in argument 2 enum */
switch(retType) {
case TYPEVOID:
Return;
break;
case TYPESTAR:
RetInteger((long)pointerSlot);
break;
case TYPEINT:
RetInteger(intSlot);
break;
case TYPEFLOAT:
RetReal(floatSlot);
break;
case TYPEDOUBLE:
RetReal(doubleSlot);
break;
case TYPESTRING:
RetString(pointerSlot);
break;
default:
RetInteger(intSlot);
break;
}
Error(500);
}
/* add a library to the dlsym search path */
/* need to pull code from src/runtime/fload.r */
int
addLibrary(int argc, descriptor argv[])
{
//union block dlBlock;
//descriptor dlBlock;
/* Add a new library to the dynamic search path */
ArgString(1)
dlHandle = dlopen(StringVal(argv[1]), RTLD_LAZY | RTLD_GLOBAL);
if (!dlHandle) {
/*
fprintf(stderr, "dlHandle error\n");
fflush(stderr);
Error(500);
*/
Fail;
}
/* The return pointer, needs to get stashed properly */
//dlBlock = mkExternal(dlHandle, sizeof(dlHandle));
//RetExternal(dlBlock);
RetInteger((long)dlHandle);
}
And an updated test head:
#
# uniffi.icn, demonstrate an experimental C FFI with libffi
#
$include "natives.inc"
link io
procedure main()
# will be RTLD_LAZY | RTLD_GLOBAL (so add to the search path)
addLibrary := pathload("uniffi.so", "addLibrary")
# allow arbitrary C functions, marshalled by libffi
native := pathload("uniffi.so", "ffi")
# add the testing functions to the dlsym search path,
# the handle is somewhat irrelevant, but won't be soonish
dlHandle := addLibrary("./testnative.so")
write("Unicon dlHandle: ", dlHandle)
write()
# pass two integers, get the sum as int
ans := native("testnative", TYPEINT, 40, 2)
write("Unicon: called testnative and got ", ans)
if ans ~= 42 then write("ERROR with testnative")
write()
# pass two reals, get the sum as real
ans := native("testdouble", TYPEDOUBLE, 9.0, 8.0)
write("Unicon: called testdouble and got ", ans)
if ans ~= 17.0 then write("ERROR with testdouble")
write()
# third arg is an integer, returns real
ans := native("testfive", TYPEDOUBLE, 1.0, 2.0, 3, 4.0, 5.0)
write("Unicon: called testfive and got ", ans)
if ans ~= 15.0 then write("ERROR with testfive")
write()
# get a pointer/handle
ans := native("teststar", TYPESTAR)
write("Unicon: called teststar and got ", ans)
if ans = 0 then write("ERROR with teststar")
write()
# a string
ans := native("teststring", TYPESTRING, "this is a string")
write("Unicon: called teststring and got ", ans)
if ans ~== "this is a string" then write("ERROR with teststring")
write()
# a string, and int and a real, returning string
ans := native("testmulti", TYPESTRING,
"for multi", 42, &pi)
write("Unicon: called testmulti and got ", ans)
if ans ~== "for multi" then
write("ERROR with testmulti")
write()
# a string, and int and a real, returning real
ans := native("testmultid", TYPEDOUBLE,
"for multid", 42, &pi)
write("Unicon: called testmultid and got ", ans)
if ans ~= &pi then write("ERROR with testmultid")
write()
#
# Variant for float versus double
#
write("float variant")
# pass two reals (as float), get the sum as real (float)
ans := native("testfloat", TYPEFLOAT,
[9.0, TYPEFLOAT], [8.0, TYPEFLOAT])
write("Unicon: called testfloat and got ", ans)
if ans ~= 17.0 then write("ERROR with testfloat")
write()
# a string, and int and a real (as float), returning real (as float)
ans := native("testmultif", TYPEFLOAT,
"for multif",
21, [&pi/2, TYPEFLOAT])
write("Unicon: called testmultif and got ", ans)
if ans - &pi/2 > 0.0000001 then write("ERROR with testmultif")
write()
#
# standard math lib
#
# todo: loader needs a path, but pathload stops, need a fail version
dlHandle := addLibrary("libm.so")
write("Unicon libm dlHandle: ", dlHandle)
# call a Bessel function double form
ans := native("j0", TYPEDOUBLE, &pi)
write("j0(pi) = ", ans)
# call Bessel function float form
ans := native("j0f", TYPEFLOAT, [&pi, TYPEFLOAT])
write("j0f(pi) = ", ans)
write()
# test switching back to the previous load library
ans := native("testfloat", TYPEFLOAT,
[9.0, TYPEFLOAT], [8.0, TYPEFLOAT])
write("Unicon: called testfloat and got: ", ans)
if ans ~= 17.0 then write("ERROR with testfloat")
write()
end
Nearly the same commands to prep the support function and build the test
heads, as listed above under C Native. The only addition is -lffi
to the
gcc compile line, and a change of filename from native.c to
uniffi.c. (I’ve taken to pronouncing that as “unify”).
prompt$ gcc -o uniffi.so -shared -fPIC uniffi.c -lffi
The testnative.c C code remains almost the same, but added
libm.so
to test the j0
and j0f
functions (and to test that loaded
libraries stick around and function lookups are cumulative).
prompt$ gcc -o testnative.so -shared -fPIC testnative.c
Here is a test run with a new top level Unicon filename uniffi.icn
with the new list [arg, TYPE]
specifiers included in some of the tests:
prompt$ unicon -s uniffi.icn -x
Unicon dlHandle: 17621520
In testnative 40 2
Unicon: called testnative and got 42
In testdouble 9.000000 8.000000
Unicon: called testdouble and got 17.0
In testfive 1.000000 2.000000 3 4.000000 5.000000
Unicon: called testfive and got 15.0
In teststar
Unicon: called teststar and got 139768978840115
In teststring with #this is a string#
Unicon: called teststring and got this is a string
In testmulti with #for multi# 42 3.141593
Unicon: called testmulti and got for multi
In testmultid with #for multid# 42 3.141593
Unicon: called testmultid and got 3.141592653589793
float variant
In testfloat 9.000000 8.000000
Unicon: called testfloat and got 17.0
In testmultif with #for multif# 21 1.570796
Unicon: called testmultif and got 1.570796370506287
Unicon libm dlHandle: 17621520
j0(pi) = -0.3042421776440938
j0f(pi) = -0.3042422235012054
In testfloat 9.000000 8.000000
Unicon: called testfloat and got: 17.0
Unified Foreign Function Interface. (Uniconified FFI). Running your Unicon
on a Mac, Windows, 32bit, 64bit, FreeBSD or a z/Linux mainframe? uniffi
has your back. Call as many library functions as you like, all from Unicon
sources, no wrappers required.
The GnuCOBOL sample listed in C Native is identical.
The libharu
integration now uses the type override system, as the PDF
generator prefers float
data arguments. It makes the listing a listing
a little less easy to read, with all the type overrides, but not too bad,
given the level of flexibility provided.
#
# haru.icn, demonstrate a newer C FFI
#
$include "natives.inc"
$define HPDF_COMP_ALL 15
$define HPDF_PAGE_MODE_USE_OUTLINE 1
$define HPDF_PAGE_SIZE_LETTER 0
$define HPDF_PAGE_PORTRAIT 0
procedure main()
local dlHandle, pdf, page1, rc, savefile := "harutest.pdf"
# will be RTLD_LAZY | RTLD_GLOBAL (so add to the search path)
addLibrary := loadfunc("./uniffi.so", "addLibrary")
# allow arbitrary C functions, marshalled by a piece of assembler
# assume float instead of double, changes the inline assembler
# movsd versus movdd
native := loadfunc("./uniffi.so", "ffi")
# add libhpdf to the dlsym search path, the handle is irrelevant
dlHandle := addLibrary("libhpdf.so")
pdf := native("HPDF_New", TYPESTAR, 0, 0)
rc := native("HPDF_SetCompressionMode", TYPEINT, pdf, HPDF_COMP_ALL)
rc := native("HPDF_SetPageMode", TYPEINT, pdf,
HPDF_PAGE_MODE_USE_OUTLINE)
$ifdef PROTECTED
rc := native("HPDF_SetPassword", TYPEINT, pdf, "owner", "user")
savefile := "harutest-pass.pdf"
$endif
page1 := native("HPDF_AddPage", TYPESTAR, pdf)
rc := native("HPDF_Page_SetHeight", TYPEINT, page1,
[220.0, TYPEFLOAT]);
rc := native("HPDF_Page_SetWidth", TYPEINT, page1,
[200.0, TYPEFLOAT]);
#/* A part of libharu pie chart sample, Red*/
rc := native("HPDF_Page_SetRGBFill", TYPEINT, page1,
[1.0, TYPEFLOAT], [0.0, TYPEFLOAT], [0.0, TYPEFLOAT]);
rc := native("HPDF_Page_MoveTo", TYPEINT, page1,
[100.0, TYPEFLOAT], [100.0, TYPEFLOAT]);
rc := native("HPDF_Page_LineTo", TYPEINT, page1,
[100.0, TYPEFLOAT],[180.0, TYPEFLOAT]);
rc := native("HPDF_Page_Arc", TYPEINT, page1,
[100.0, TYPEFLOAT], [100.0, TYPEFLOAT],
[80.0, TYPEFLOAT], [0.0, TYPEFLOAT],
[360 * 0.45, TYPEFLOAT]);
#pos := native("HPDF_Page_GetCurrentPos (page);
rc := native("HPDF_Page_LineTo", TYPEINT, page1,
[100.0, TYPEFLOAT], [100.0, TYPEFLOAT]);
rc := native("HPDF_Page_Fill", TYPEINT, page1);
rc := native("HPDF_SaveToFile", TYPEINT, pdf, savefile);
native("HPDF_Free", TYPEVOID, pdf);
end
What comes next? That is up to the creativity, imagination and needs of august Unicon developers. No need to write any C to get at C, or any language that uses the C application binary interface. As a (not completely) informed guess, I’d peg that at well over 75% of currently available computing resources, world wide. Even an Android phone plays nice with the C ABI internally.
Other features of libffi
can be exposed as needs are determined. The
ffi_call
layer supports sysv
, unix64
, win64
, stdcall
,
fastcall
, thiscall
and cdecl
call conventions. Possibly others,
depending on platform specific builds. By default, uniffi
uses the calling
convention most appropriate for the system used during builds by using
FFI_DEFAULT_ABI
when preparing the Call InterFace block.
baconffi¶
Putting libffi to use with BaCon, the BASIC Converter.
Like the GnuCOBOL example, this routine just sums two numbers passed in as arguments and returns the resulting integer.
REM basicnative.bac, uniffi call to BaCon BASIC
FUNCTION basic(NUMBER one, NUMBER two)
RETURN one + two
END FUNCTION
programs/uniffi/basicnative.bac
This source is then converted to C, and a shared library is created.
prompt$ bacon -q -f basicnative.bac
Converting 'basicnative.bac'...
Converting 'basicnative.bac'... done, 15 lines were processed in 0.003 seconds.
Compiling 'basicnative.bac'... make[1]: Entering directory '/home/btiffin/wip/writing/unicon/programs/uniffi'
cc -fPIC -c basicnative.bac.c
cc -o basicnative.so basicnative.bac.o -L. -lbacon -lm -shared -rdynamic
make[1]: Leaving directory '/home/btiffin/wip/writing/unicon/programs/uniffi'
Done, program 'basicnative.so' ready.
A slightly more sophisticated BaCon program, that embeds some inline assembler. Sample derived from a thread on the BaCon forums by vovchik and Axelfish, http://basic-converter.proboards.com/thread/752/assembler-bacon
REM mixedbacon.bac, uniffi call to BaCon BASIC
FUNCTION embed(int operand1,int operand2) TYPE int
USEC
#define ASM asm(
#define GAS );
int sum, accumulator;
ASM
"movl %1, %0\n\t"
"addl %2, %0"
: "=r" (sum) /* output operands */
: "r" (operand1), "r" (operand2) /* input operands */
: "0" /* clobbered operands */
GAS
accumulator = sum;
ASM
"addl %1, %0\n\t"
"addl %2, %0"
: "=r" (accumulator)
: "0" (accumulator), "g" (operand1), "r" (operand2)
: "0"
GAS
END USEC
RETURN accumulator
END FUNCTION
REM
REM asmmix, to be called from Unicon uniffi
REM
FUNCTION asmmix(int one, int two)
PRINT "BaCon received: ", one, ", ", two
my_accumulator=embed(one, two)
RETURN my_accumulator
END FUNCTION
Again, convert the source and compile to a shared library.
prompt$ bacon -q -f asmmix.bac
Converting 'asmmix.bac'...
Converting 'asmmix.bac'... done, 48 lines were processed in 0.005 seconds.
Compiling 'asmmix.bac'... make[1]: Entering directory '/home/btiffin/wip/writing/unicon/programs/uniffi'
cc -fPIC -c asmmix.bac.c
cc -o asmmix.so asmmix.bac.o -L. -lbacon -lm -shared -rdynamic
make[1]: Leaving directory '/home/btiffin/wip/writing/unicon/programs/uniffi'
Done, program 'asmmix.so' ready.
A Unicon test head to load in the basic
function defined in
basicnative.bac
and then invoke the function through the libffi
interface. Same sequence for the asmmix
call.
#
# baconffi.icn, test calling BaCon without wrapper with libffi
#
$include "natives.inc"
procedure main()
# will be RTLD_LAZY | RTLD_GLOBAL (so add to the search path)
addLibrary := loadfunc("./uniffi.so", "addLibrary")
# allow arbitrary C functions, marshalled by libffi
native := loadfunc("./uniffi.so", "ffi")
# add the testing functions to the dlsym search path,
# the handle is somewhat irrelevant, but won't be soonish
dlHandle := addLibrary("./basicnative.so")
# pass two integers, get back a sum
write("Unicon calling BaCon function \"basic\" with 40 and 2")
ans := native("basic", TYPEINT, 40, 2)
write("Unicon got ", ans)
# pass two integers that create an accumulator in assembly
dlHandle := addLibrary("./asmmix.so")
write("Unicon calling \"asmmix\" with 2 and 4")
ans := native("asmmix", TYPEINT, 2, 4)
write("Unicon accumulator of 2+4 + 2+4 + 2: ", ans)
end
Sample run:
prompt$ unicon -s baconffi.icn -x
Unicon calling BaCon function "basic" with 40 and 2
Unicon got 42
Unicon calling "asmmix" with 2 and 4
BaCon received: 2, 4
Unicon accumulator of 2+4 + 2+4 + 2: 14
And mixing Unicon with BASIC becomes another easy thing to do.
BaCon is an extraordinarily powerful BASIC translator. Lots of features.
http://www.basic-converter.org/
Index | Previous: Programs | Next: Theory