Tried Solutions are not Tired Solutions

This article provides a great argument to why a tried-and-tested database like the one provided in GT.M is a better solution than the trendy, buzzword-compliant, Johnny-come-lately industry darling of the day. If a social networking package finds MongoDB’s consistency problems unacceptable, imagine the disaster lurking for its use in the kinds of apps currently being supported with MUMPS databases! Also, the problems inherent in querying these kinds of databases for any but the most simple set of predicates is barely even covered.

While one could argue that MUMPS suffers from a lack of sex appeal that keeps it a little-known and oft-ridiculed language, let’s remember that the resilience of our flagship VistA–in spite of its mother agency’s constant and concerted efforts to kill it–is in large part due to the small and dedicated community that has built up around supporting and improving it. Perhaps we should entertain the idea that a smaller, more focused community of good people is better for our language and its applications than a large and well-known group, which would invariably be fated to follow the unstable and expensive path dictated by the wiles of a fickle and short-sighted industry.

Long story short, we should focus on improving the existing MUMPS language and database, and be extremely careful to avoid being distracted by the lure of bright, shiny objects. There are no silver bullets, and anything that sounds too good to be true probably is.

Advertisements

Configure Production GT.M Instance on OpenVMS

In this article, we will begin to set up a production-grade instance of GT.M on OpenVMS/Alpha. This instance of GT.M will have journaling enabled, as well as having an optimized data storage configuration. We will assume that GT.M has already been installed with the defaults, and that the GT.M logical names from GTM$DIST:GTMLOGICALS.COM are defined.

Contents

Document Conventions

User-supplied parameters will be enclosed in brackets, i.e. <my-parameter>. Where these appear, you will be expected to replace the bracketed parameter with the appropriate values for your system.

The bracketed parameters are listed below:

<home-device>
The OpenVMS volume containing the instance user’s home directory
<journal-device>
The OpenVMS volume containing the instance’s journal files
<data-device>
The OpenVMS volume containing the instance’s global directory and data files
<image-device>
The OpenVMS volume containing the instance’s object files (*.O;*)
<instance-user>
The username of the instance user
<instance-user-name>
The name (i.e. “Test User”, “Development User”, or “Production User”) of the instance user
<group-number>
The group number component of the instance user’s UIC
<user-number>
The user number component of the instance user’s UIC

Physical Storage

This setup will require four storage volumes, in addition to any system and data volumes already in use on your system. Each volume may actually consist of multiple devices in a RAID set. Thus, when we refer to a storage volume, it may refer to any number or configuration of physical storage devices. Storage volumes have names like DKA400: or DKC0: in OpenVMS.

Please choose four storage volumes that are otherwise unused, as this procedure will destroy all existing data on the four volumes chosen.

Home Volume

Referred to as <home-device> in the DCL examples that follow, the home volume contains the instance user’s home directory. The instance user’s home directory will contain two subdirectories, one named “R” and one named “P”. The “R” subdirectory (“ROUTINES”) will contain the source files for any apps defined on the system, and will be writable only by SYSTEM. The “P” subdirectory (“PATCHES”) will contain source code and object files for locally-generated routines and modifications. When executing MUMPS routines in this configuration, routines stored in “P” will override routines stored in “R”, when both routines have the same filename.

When choosing a RAID configuration for the Home Volume, the configuration should favor fault tolerance over speed, and be optimized for sequential I/O.

For this phase of the procedure, and until otherwise specified, I will assume that you are logged into the SYSTEM account.

Let us begin by initializing and mounting the Home Volume, as shown below:

$ INITIALIZE <home-device>: GTMHOME
$ MOUNT/SYSTEM <home-device> GTMHOME

Journal Volume

Referred to as <journal-device> in the DCL examples that follow, the journal volume contains the GT.M journal files for this instance. The journal files provide increased availability for the instance by facilitating recovery when power outages or other events prevent the GT.M database files from being properly quiesced (or “run down” in GT.M parlance).

When choosing a RAID configuration for the Journal Volume, the configuration should favor fault tolerance over speed, and be optimized for sequential I/O, as journal files are only appended to, or read from beginning to end.

We will now initialize and mount the Journal Volume, as shown below:

$ INITIALIZE <journal-device>: GTMJNL
$ MOUNT/SYSTEM <journal-device> GTMJNL

Data Volume

Referred to as <data-device> in the DCL examples that follow, the data volume contains the GT.M global directory and data files for this instance. This is where GT.M will store MUMPS globals, and is arguably the most important volume of all.

When choosing a RAID configuration for the Data Volume, the configuration should balance fault tolerance and speed, and be optimized for random access I/O, as GT.M data will be accessed from unpredictable positions within the data files.

Let’s initialize and mount the Data Volume, as shown below:

$ INITIALIZE <data-device>: GTMDATA
$ MOUNT/SYSTEM <data-device> GTMDATA

Image Volume

Referred to as <image-device> in the DCL examples that follow, the image volume contains the GT.M object files for this instance. These are the actual binaries that will be run by the GT.M environment.

When choosing a RAID configuration for the Image Volume, the configuration should favor speed over fault tolerance, as access times for the object files will directly affect your application’s load times and performance, and object files can typically be regenerated from their respective sources.

Time to initialize and mount the Image Volume, as shown below:

$ INITIALIZE <image-device>: GTMIMG
$ MOUNT/SYSTEM <image-device> GTMIMG

Instance User and Directories

In this step, we will create the user account for the instance user, and create the necessary directories where GT.M will store its data, routines, object files, journals, and local patches.

Multiple users could be created to support multiple instances, but we will focus only on creating one user and the necessary directories. The same procedure applies for creating further instance users.

In the DCL examples that follow, please consult the table of bracketed parameters from Part 1 of this tutorial for definitions of <instance-user>, etc.

We will begin by running the OpenVMS User Authorization Facility (UAF) and adding the user, as shown below:

$ SET DEF SYS$SYSTEM
$ RUN AUTHORIZE
UAF> ADD <instance-user>/PASSWORD=temp/OWNER="<instance-user-name>"/DEV=<home-device>/DIR=[<instance-user>]/UIC=[<group-number>,<user-number>]/FLAG=NODISUSER
%UAF-I-PWDLESSMIN, new password is shorter than minimum password length
%UAF-I-ADDMSG, user record successfully added
%UAF-I-RDBADDMSGU, identifier <instance-user> value [<group-number>,<user-number>] added to rights database
UAF> EXIT
%UAF-I-DONEMSG, system authorization file modified
%UAF-I-RDBDONEMSG, rights database modified

Now we will create the necessary directory structure, as shown below:

$ CREATE/DIRECTORY <home-device>:[<instance-user>]
$ CREATE/DIRECTORY <home-device>:[<instance-user>.r]
$ CREATE/DIRECTORY <home-device>:[<instance-user>.p]
$ CREATE/DIRECTORY <journal-device>:[<instance-user>]
$ CREATE/DIRECTORY <journal-device>:[<instance-user>.j]
$ CREATE/DIRECTORY <data-device>:[<instance-user>]
$ CREATE/DIRECTORY <data-device>:[<instance-user>.g]
$ CREATE/DIRECTORY <image-device>:[<instance-user>]
$ CREATE/DIRECTORY <image-device>:[<instance-user>.o]
$ CREATE/DIRECTORY <image-device>:[<instance-user>.o.50000]

Now, set the ownership on the directories:

$ SET DIRECTORY/OWNER=<instance-user> <home-device>:[<instance-user>]
$ SET DIRECTORY/OWNER=<instance-user> <home-device>:[<instance-user>.r]
$ SET DIRECTORY/OWNER=<instance-user> <home-device>:[<instance-user>.p]
$ SET DIRECTORY/OWNER=<instance-user> <journal-device>:[<instance-user>]
$ SET DIRECTORY/OWNER=<instance-user> <journal-device>:[<instance-user>.j]
$ SET DIRECTORY/OWNER=<instance-user> <data-device>:[<instance-user>]
$ SET DIRECTORY/OWNER=<instance-user> <data-device>:[<instance-user>.g]
$ SET DIRECTORY/OWNER=<instance-user> <image-device>:[<instance-user>]

Now, we will set permissions on the newly-created directories, so that only SYSTEM will be able to write or delete object files in <image-device>:[<instance-user>.o]50000.DIR, as shown below:

$ SET SECURITY /PROTECTION=(S:RWED,O:RE,G:RE,W:"") <image-device>:[<instance-user>.o]50000.DIR
$ SET SECURITY /PROTECTION=(S:RWED,O:RE,G:RE,W:"") <image-device>:[<instance-user>]O.DIR

You will next need to create <home-device>:[instance-user>]LOGIN.COM, including the lines of DCL code shown below; these lines will define the correct values for GTM$GBLDIR (which determines where the GT.M global directory is located) and GTM$ROUTINES (which determines the locations GT.M will search for routines and object files) are set.

$ IF (P1 .NES. "") .AND. (F$EXTRACT(0,1,P1) .NES. "/") THEN P1 := /'P1
$ DEFINE 'P1' GTM$GBLDIR	<data-device>:[<instance-user>.g]MUMPS.GLD
$ DEFINE 'P1' GTM$ROUTINES	 "<home-device>:[<instance-user>.p],<image-device>:[<instance-user>.o.50000]/SRC=<home-device>:[<instance-user>.r],GTM$DIST:"
$ EXIT

Next, add the following lines to SYS$MANAGER:SYSTARTUP_VMS.COM. This will ensure that the newly-created volumes are available:

$ MOUNT/SYSTEM <home-device GTMHOME
$ MOUNT/SYSTEM <journal-device> GTMJNL
$ MOUNT/SYSTEM <data-device> GTMDATA
$ MOUNT/SYSTEM <image-device> GTMIMG

Defining the Global Directory and Creating the Data File

In this installment, we will use the GT.M Global Directory Editor (GDE) and the MUMPS Peripheral Interchange Program (MUPIP) to define the global directory and database file for the instance.

For this instance, you will need to be logged into the <instance-user> account created in the prior installment. This is crucial.

 Historical Note

The MUPIP program’s name has very deep roots. A Peripheral Interchange Program (PIP) was first used in the Digital Equipment Corporation PDP-6 series of computers in the early 1960’s, and later made it into TOPS-10 on the PDP-10, RSTS/E on the PDP-11, and eventually into Gary Kildall’s CP/M operating system, which is largely credited as an early and important foundation of the personal computer revolution. How it got into GT.M is a bit of trivia with which I am as yet unacquainted, but perhaps someone here can shed a little light on the subject.

So, without further ado, here are the commands used to set up your global directory:

$ RUN GTM$DIST:GDE
GDE> CHANGE /SEGMENT $DEFAULT /FILE=<data-device>:[<instance-user>.g]MUMPS.DAT /ALLOC=200000 /BLOCK_SIZE=4096 /LOCK_SPACE=1000 /EXTENSION_COUNT=0
GDE> CHANGE /REGION $DEFAULT /RECORD_SIZE=4080 /KEY_SIZE=255

The above commands bear further explanation.

The first line is the DCL command which will launch the GT.M Global Directory Editor, and should be familiar to anyone who has a passing familiarity with OpenVMS and DCL.

The second line sets the characteristics of the $DEFAULT database segment. The /FILE switch tells GDE to use <data-device>:[<instance-user>.g]MUMPS.DAT to store the data for the segment. The /ALLOC and /BLOCK_SIZE switches instruct GDE to allocate 200,000 blocks of 4,096 bytes each to the segment. The /LOCK_SPACE instructs GDE to allocate 1,000 pages for locking, which can prevent deadlocks under heavy load. The /EXTENSION_COUNT=0 switch instructs GT.M to disable its ability to automatically expand the database when storage grows short. Although you can set EXTENSION_COUNT to a rather arbitrary number of blocks, I do not recommend this practice, as the consequences of filling up your data drive at the OpenVMS level can be more catastrophic than filling up your database file, which will simply halt further writes to the database. A good solution is to employ a script to monitor database usage and notify you when a certain threshold is reached.

It is worth noting that you can calculate your database size by multiplying /BLOCK_SIZE by /ALLOC. In this case, the database will be slightly over 781MB (200,000 blocks * 4,096 bytes per block).

Next, we will set up journaling using the MUPIP program.

Journaling and MUPIP

The following commands will enable journaling to <journal-device>:[<instance-user>.j]<instance-user>.MJL:

$ RUN GTM$DIST:MUPIP
MUPIP> CREATE
Database file for region $DEFAULT created.
$ RUN GTM$DIST:MUPIP
MUPIP> SET /REGION $DEFAULT /JOURNAL=(ENABLE,ON,BEFORE,FILENAME=<journal-device>:[<instance-user>.j]<instance-user>.MJL)
%GTM-I-JNLCREATE, Journal file <journal-device>:[<instance-user>.j]<instance-user>.MJL created for region $DEFAULT
 with BEFORE_IMAGES
%GTM-I-JNLSTATE, Journaling state for region $DEFAULT is now ON

CREATE tells MUPIP to create the .DAT file as specified by the global directory.

The command containing SET /REGION tells MUPIP to enable journaling for region $DEFAULT. ENABLE tells MUPIP that the specified region is ready to be journaled. ON tells MUPIP to create a new journal file (as specified by FILENAME) and begin using the newly-created file to record future journal entries. BEFORE instructs GT.M’s journaling system to archive data blocks prior to modifying them, and enables the use of the rollback recovery facility on the specified region.

Now that the database is being journaled, we need only to set the ownership and permissions on MUMPS.DAT and MUMPS.GLD to prevent unauthorized access. This procedure is detailed in the DCL example below:

$ SET FILE/OWNER=<instance-user> <data-device>:[<instance-user>.g]MUMPS.GLD
$ SET FILE/OWNER=<instance-user> <data-device>:[<instance-user>.g]MUMPS.DAT
$ SET SECURITY /PROTECTION=(S:RWED,O:RWE,G:RWE,W:"") <data-device>:[<instance-user>.g]MUMPS.GLD
$ SET SECURITY /PROTECTION=(S:RWED,O:RWE,G:RWE,W:"") <data-device>:[<instance-user>.g]MUMPS.DAT

The instance is now created with journaling and security protections in place. You can now install any local MUMPS applications’ routines into <home-device>:[<instance-user>.r].

External Links

GT.M Administration and Operations Guide for OpenVMS

Using GT.M external calls to access shared libraries

Many times, when developing MUMPS applications, you may come upon a situation where you need to use a bit of functionality exposed by a Linux shared library. FIS GT.M provides support for this via its external call mechanism. The syntax and semantics are a bit odd, so we’ll step through the implementation of a wrapper for some of the  trigonometry functions exposed by the C standard library. This example will be produced with the intention of being complete and usable.

Assumptions on the Reader

I will assume that you have a reasonably recent GT.M release configured on a Linux system. I will assume that you have access to a bash shell prompt (for those of you using VistA, a captive account which takes you directly into a VistA entry point is not sufficient for this tutorial, as you will be creating several files in the Linux filesystem with tools which don’t exist in or are inaccessible from the GT.M programmer mode. If this applies to you, please see your local guru for help).

I will assume that your local GT.M routines are in $HOME/p, that $gtmroutines is set accordingly, that you have a $HOME/lib directory available, and that your GT.M environment is set up to the point where you can read and set MUMPS globals.

I will also assume that you have working knowledge of MUMPS and C programming, including basic knowledge of pointers and their use for the latter language.

I will also assume that you have gcc and make installed, and that you or your system manager has made them available in your search path. If you are working through this example on your own machine, here are some instructions for getting gcc and make running:

Ubuntu

$ sudo apt-get install build-essential

Other Distributions

For other distributions, please search Google.
First, let’s talk a bit about the overall architecture of the GT.M external call mechanism.

Architecture

The GT.M external call mechanism uses a layered architecture. The GT.M runtime looks in the GTMXC_yourcalltable environment variable to find the location of a .xc file which contains the GT.M to C mappings. The .xc file also contains a path to a shared library (a file ending with the .so extension) in which the external routines are defined. Here’s an abstract overview of what the architecture looks like:

GT.M Callout Architecture

Figure 1.1: Abstract GT.M external call architecture

If we were creating new functionality without trying to access an existing shared library, the wrapper_library.so and wrapped_library.so pieces of the stack would likely be replaced with a single .so file containing the new functionality.

In this case, we’ll derive from this abstract architecture a more concrete architecture to apply to our trigonometry wrapper:

Concrete Architecture

Figure 1.2: Concrete architecture for our applicaton

GT.M Type System

When working with external calls to non-MUMPS shared libraries in GT.M, you need to first come to terms with the fact that you are going to end up writing C wrapper functions for every function you use. Unfortunately, GT.M lacks the intelligence to directly call external functions of arbitrary types and parameter lists, and requires an external call table to map the weakly-typed data of C to the untyped data of MUMPS.

Return Values

In order for an external function to be callable by GT.M, it can only return one of three types:

  • gtm_long_t (a long integer)
  • gtm_status_t (an int)
  • void (function does not return a value)

For our purposes, we will use gtm_status_t for as the return values’ types. This will allow us to return a 0 value from our wrapper functions when successful. Returning a nonzero value will indicate to GT.M that an error has occurred. For the sake of clarity, we will leave extensive error handling as an exercise to the reader.

Parameters

Each parameter can be either an input parameter (GT.M passes the value of this parameter to C) or an output parameter (GT.M passes a reference to this parameter to C, which populates it with a return value). Parameters can be any of the types listed in Chapter 11 of the GT.M UNIX Programmer’s Guide. For our purposes, we will be using gtm_double_t* for both our input parameters and output parameters.

External Call Table

Okay, we’re now ready to look at the format of the external call table (trig.xc in our example). The first line must be a full UNIX path to the shared library that GT.M will call, for example:

$HOME/lib/trig.so

This will tell GT.M to look in /home/your_username/lib/trig.so when trying to resolve the functions defined within the trig.xc external call table.

The remainder of the external call table is a list of definitions which map C functions to GT.M routines; one per line. Ours will look like this:


sin: gtm_status_t m_sin(I:gtm_double_t*, O:gtm_double_t*)
cos: gtm_status_t m_cos(I:gtm_double_t*, O:gtm_double_t*)
tan: gtm_status_t m_tan(I:gtm_double_t*, O:gtm_double_t*)

Using the sin function, let’s break down the format of one of these lines:

  • sin: is the name by which this function will be referred when called by our MUMPS code
  • gtm_status_t is the data type which will be returned by our C wrapper
  • m_sin is the name of our C wrapper function
  • I:gtm_double_t* specifies that the first parameter of our C wrapper is a pointer to a double-precision floating point value. The I specifies that this parameter is used for input to our C wrapper. In this case, this is the number to which the sin function will be applied.
  • O:gtm_double_t* specifies that the second and final parameter of our C wrapper is a pointer to a double-precision floating point value. The O (output) specifier indicates that GT.M will be passing a variable by reference for this parameter, and that our C wrapper will be populating it with a return value.

Here’s the completed external call table, trig.xc:


$HOME/lib/trig.so
sin: gtm_status_t m_sin(I:gtm_double_t*, O:gtm_double_t*)
cos: gtm_status_t m_cos(I:gtm_double_t*, O:gtm_double_t*)
tan: gtm_status_t m_tan(I:gtm_double_t*, O:gtm_double_t*)

C Wrapper Functions

For our C wrapper functions, there are a couple of important conventions to note:

  • The first parameter to each wrapper function must be an int. Although this is not (and must not be) specified in the external call table (trig.xc), it must be included in each wrapper function. GT.M will automatically pass to this parameter a value indicating the total number of parameters passed to our wrapper function. It is essentially the GT.M external call mechanism’s own implicit version of argc.
  • We must tell the preprocessor to include gtmxc_types.h which is located in $gtm_dist

Let’s look at the complete trig.c:

#include <math.h>
#include "gtmxc_types.h"

gtm_status_t m_sin(int c, gtm_double_t *x, gtm_double_t *out)
{
    *out = sin(*x);
    return(0);
}

gtm_status_t m_cos(int c, gtm_double_t *x, gtm_double_t *out)
{
    *out = cos(*x);
    return(0);
}

gtm_status_t m_tan(int c, gtm_double_t *x, gtm_double_t *out)
{
    *out = tan(*x);
    return(0);
}

The points that bear further discussion are the function definitions, such as m_sin(int c, gtm_double_t *x, gtm_double_t *out), and the pointer assignments, such as *out = sin(*x);

The function definitions are unique in that they use the typedefs (defined in $gtm_dist/gtmxc_types.h) for the return type and parameter types. This should facilitate portability among the various flavors of UNIX and Linux that GT.M supports.

The assignments use pointers so that the output value (in this case, gtm_double_t *out) can be accessed from within the GT.M environment. The assignment *out = sin(*x); means that we are assigning the value of the sin function of the data pointed to by x into the memory location pointed to by out. This is what allows GT.M to retrieve the value from within its native environment. When dealing with C pointers, I find it useful to read the “flow” of the operation from right-to-left.

MUMPS Routine

Next, we will build a MUMPS routine to hide our use of the GT.M external call mechanism. This is a good idea in case you ever need to port your MUMPS application to a platform that uses different mechanisms for external calls, such as InterSystems Cache’.

Here is our MUMPS routine, trig.m:

trig ;; trigonometry wrappers

sin(num)
 new result
 do &trig.sin(num,.result)
 quit result

cos(num)
 new result
 do &trig.cos(num,.result)
 quit result

tan(num)
 new result
 do &trig.tan(num,.result)
 quit result

Although you could call the external routines directly, the wrapper-within-a-wrapper approach provides more readable code, hides the call-by-reference from the programmer using your routines, and gives you MUMPS code that is more idiomatically representative of the 1995 MUMPS ANSI standard.

The uniqueness of this routine is in the &wrapper.function() syntax. The &trig.function() syntax instructs GT.M to check the $GTMXC_trig environment variable to find the external call table it should use to execute function(). In official GT.M parlance, trig is a package. There is also a $GTMXC environment variable, which points to the callout table for what is referred to as the “default” package, but in the interests of portability and modularity, we will not cover its use here.

Compiling and Linking

Now that we have our MUMPS code (trig.m), our callout table (trig.xc), and our C wrappers (trig.c), we can create a Makefile to compile and link our wrapper functions into a shared library. Please note that this makefile is specific to Linux and may or may not work on other UNIX or UNIX-like operating systems.

CFLAGS=-Wall

all: trig.so

trig.so: trig.o
        gcc $(CFLAGS) -o trig.so -shared trig.o -lm

trig.o: trig.c
        gcc $(CFLAGS) -c -fPIC -I$(gtm_dist) trig.c

clean: 
        rm trig.so
        rm trig.o

install:
        cp trig.so $(HOME)/lib

Let’s break this down line by line:

  • CFLAGS=-Wall

This line gives us a variable for flags that we will always pass to the C compiler. -Wall instructs the compiler to enable all warning messages. This should always be used, as it will help you to write cleaner code.

  • all: trig.so

This is the first rule of the Makefile, which will be invoked automatically if make is run with no command-line arguments. It simply says that the rule “all” depends on “trig.so” to be considered complete. So, if “trig.so” does not exist, make will then scan the Makefile to find a rule to use to build “trig.so”

  • trig.so: trig.o

This is the rule for building “trig.so”. It simply means that trig.so depends on the existence of “trig.o” in order to build it. If “trig.o” does not exist, make will search for a rule to build “trig.o”

  • gcc $(CFLAGS) -o trig.so -shared trig.o -lm

This is the command necessary to generate “trig.so” from “trig.o”. The “-o” flag tells the linker to name the output “trig.so”. The “-shared” flag tells the linker that we wish to generate a shared library from “trig.o”. “-lm” tells the linker that this production depends on libm.so (the “-l” flag automatically prepends “lib” onto the library we specify. “-llibm” does not work).

  • trig.o: trig.c

This is the rule from building “trig.o” from “trig.c”. It informs make that trig.o requires trig.c in order to be built. Since there is no rule in this Makefile for generating “trig.c”, make will look for “trig.c” in the current working directory.

  • gcc $(CFLAGS) -c -fPIC -I$(gtm_dist) trig.c

This is the command necessary to generate “trig.o” from “trig.c”. The “-c” flag instructs gcc to compile, but not link, the specified file. The “-fPIC” flag instructs gcc to generate position-independent code, which means that the symbols in the object file may be relocated and resolved at load time rather than link time, which is necessary for shared libraries. The “-I$(gtm_dist)” flag tells the compiler that it should search $(gtm_dist) for header (.h) files. This is necessary because gtmxc_types.h will not likely be in a place that the compiler knows about. $(gtm_dist) is an environment variable which is required in order for GT.M to function, and will always contain the path in which gtmxc_types.h is located.

Putting it all together

When you have trig.m, trig.c, trig.xc, and Makefile typed in, run the following commands from the shell prompt:

$ cp trig.m $HOME/p/
$ make
$ make install
$ export GTMXC_trig=$HOME/lib/trig.xc
$ mumps -dir
GTM> w $$sin^trig(2.53)

GT.M will respond by writing .574172 to the screen.

Where to go next

Refer to the GT.M UNIX Programmer’s Manual for more information on advanced uses of the GT.M external call mechanism.

I hope this article proves useful, and welcome your feedback!