Home page Home page Home page Home page
Pixel
Pixel Header R1 C1 Pixel
Pixel Header R2 C1 Pixel
Pixel Header R3 C1 Pixel
Pixel
By Sprezz | Wednesday, 10 June 2026 10:51 | 0 Comments

I'm sure that we've all had to deal with the sort of user interface where we are working on a data aware form and only want to enable the OK/Save button when a series of conditions (other than simply required which the 4GL can happily enforce) are met. And like me you've probably written a generic validateOKRequirements: subroutine and wired it into every relevant LOSTFOCUS, CHANGED, CLICK event.

Tedious, but necessary.

Or so I thought.

Enter SAVEWARN and SYSMSG.

If you're thinking, "Oh I know about that" then please resume normal programming. If, like me, you apparently have been living under a rock for all these years then read on.

The SAVEWARN property of the Window is a "Dirty" flag. If it is true then the data on the screen has changed from how it was when it was originally loaded. This is the flag the system checks the generate the "Changes will be lost... Continue?" prompt. 

So to enable/disable our own button all we need to do it check the value of SAVEWARN and if it is true, gosub our validation, and enable/disable accordingly.

All well and good - but the thing that has evaded me all of these years, was how to actually do that. It seems the answer has been staring me in the face for years. The system already tells you when something becomes dirty and it does so through code 21 with SYSMSG.. 

The SYSMSG event is raised to a Window to allow the developer to intercept and modify/replace system responses to such situations as "Data will be lost, continue?" or "Are you sure you wish to delete?". Each of these passes the SYSMSG event a code to identify itself, such as 1 for the former and 2 for the latter.

But scan to the end of the list and there at 21 is 

 equ SYSMSG_SAVEWARNINFO$      to 21  ; // Save warn has been changed - null msg

This is triggered each time SAVEWARN changes state. So when it becomes true and when it becomes false. So now we can do away with all of our LOSTFOCUS,CLICK,CHANGE type events and instead simply have one SYSMSG event with code like the following



thereby centralising all of the checking in one easy to use place. 
By Sprezz | Wednesday, 3 June 2026 12:40 | 0 Comments

We would label this a tricky gotcha but truth be told BUT if it got you you must have had more time on your hands than sense.

A recent announcement from Revelation  explained that the <> syntax has been updated - and that due to huge improvements in <>< processing speeds " you can continue using the familiar “<>” syntax for sequential dynamic array processing without needing to rewrite code using LOOP/REMOVE or LOOP/[] parsing patterns.". 

Historically nobody in their right mind would have processed the 65,536th element of a dynamic array using <>. If they wanted to get there they'd have used loop/remove. When the Revelation compiler was originally written, the maximum row size was 64K - so the maximum possible populated columns was 32K. So to speed up processing, the compiler replaced a < N > reference to a special opcode optimised for such extractions. Using < N, N > and < N, N, N > don't redirect to this special opcode, redirecting to the EXTRACT opcode instead.

All well and good except for one slight snag. Remember the 64K limit? Well, then it made sense to store the value for the <> opcode as an unsigned 32 bit integer. Trouble is, that can only accommodate 64K.

So what happens if you try and extract from an array where N is > 64K?

A picture allegedly speaks louder than words


So if you would like to protect yourself from this rollover, then the easiest way is just to force the compiler to use EXTRACT by referencing a multivalue.



In fairness this only happens when you reference an array element with a constant - if you use a variable you're safe. If you absolutely MUST use a constant greater than 65,535 then the good news is that this behaviour will be fixed in OI11!





By Sprezz | Wednesday, 7 January 2026 16:37 | 0 Comments

One of the best parts of the new IDE is that every control type has its own designer: open it in-place, tweak the editable properties, and inspect the repository metadata. We live in these designers every day when we open a form, a program, a popup, or even a menu.

What’s less obvious is that there’s also a designer for object code. It turns a lot of “system sleuthing” from writing code to take object code apart into something closer to browsing.

To get to it: File → Open → Entity, then choose Stored Procedure Executables.


Here's an example of using the designer to look at RLIST


Open RLIST and the first thing you notice is how readable fragments are more easily visible due to them being plain text. Once you recognise the layout, that visibility gives you quick signals about what the routine is doing and which external routines it calls.

On the right, the ObjectCode Properties panel tells you the basics immediately: RLIST is a subroutine (not a function) and it takes five parameters. The parameter names are obscured, but the Options button expands a popup showing the full list clearly.

That’s the tool in the abstract. Here’s where it becomes genuinely useful.

A concrete use: “How many users are in the system right now?”

You can think of some theoretical ways of approaching the problem, but someone else must have had this issue surely? Chances are someone’s already solved this inside RTI — so let’s search.


We can safely discard most of those if what we are looking for is a routine from RTI to GET the USERCOUNT.

Let's try opening that up in the designer


It's a function. If you call it with three blank variables, it populates them with:

  • current active user count
  • total licence count
  • licences remaining

One caveat: it only works when called with the UD in place.

I’m sure you’ll find your own favourite uses for the object code designer, but if you haven’t tried it yet, it’s worth five minutes. If you’ve ever had to take object code apart by hand, this saves real time.
By Sprezz | Wednesday, 19 November 2025 12:26 | 3 Comments

OI 10 marked the introduction of a new licensing method, replacing the old `oengine.dll` stamping. Historically, the licensing information was baked into `oengine.dll`. If you wanted to “change” the characteristics of your OI system you could just drop in a different `oengine.dll` and, effectively, you had a new system.

OI 10 introduces a separate licence metadata file called `revengine.lic`. It’s a simple XML file that looks like this (with some minor obfuscation):

<OI>
    <Signature>PEe80-Uf189-5mg27-m1t80-00m4Q-9hih7</Signature>
    <SerialNumber>W10280784</SerialNumber>
    <NumUsers>10</NumUsers>
    <ExpirationDate>2025-05-21</ExpirationDate>
    <ExpirationType>0</ExpirationType>
</OI>

The `Signature` element is the important part. It’s a cryptographic signature over the real licence attributes – serial number, user count, expiry date and type – generated with a secret key that only Revelation knows. OI trusts *that* value and treats the rest of the XML purely as metadata for human consumption. You can edit `SerialNumber`, `NumUsers`, `ExpirationDate` and `ExpirationType` to your heart’s content; it doesn’t change the way OI behaves because those fields are not trusted.

At this point you might think: “Fine, I’ll just copy the `.lic` file instead of the `.dll`.”

But there’s a catch.

OI 10 also moves to industry-standard security. Various user rows in the system are now encrypted using a key derived from (and salted with) the Signature. Change the Signature and you change the encryption seed. Anything that was encrypted under the old Signature will no longer decrypt under the new one and, if you’re using the default security policy, you will lock yourself out of the system.

So yes, `revengine.lic` is technically portable – but once you’ve started using the system in anger, all of your sensitive user data is cryptographically tied to that specific licence Signature. Swapping licences is no longer a harmless way to “refresh” a system; it has real consequences for your encrypted data.

By Sprezz | Monday, 9 June 2025 13:54 | 0 Comments

An old client recently got in contact about the system we had written for them in AREV in the early 90s and subsequently ported to AREV32. Over the years, we had added some bells and whistles including an incredibly specifically tailored OI export utility which we installed about 10 years ago.

Just recently, we added another OI export utility, and it was working as expected. But now, the client has reported that when including a specific set of columns, the report just hung - no messages, nothing. Eventually they just had to kill it in the task manager. They wondered if this was in any way connected to the recent upgrade?

Now here's where it gets interesting. The columns in question are used frequently throughout the system, displayed on entry screens, reports and the like with no issues, so we thought the client's concern might be justified.

We spun up our test system and attempted to produce the report the client was trying to create. Immediately upon selecting the columns in question, the system broke into the debugger with 'Unable to load program XXX'.

At least this explained why the report was failing (the live system runs with the debugger disabled so they wouldn't see the error message). But it didn't explain how anything we might had done in the latest upgrade could have caused this.

So down to TCL and EDIT BP XXX.

The program is there. Perhaps it hadn't been compiled? Recompile. The issue was not resolved. 

Maybe, just maybe it hadn't been cataloged? EDIT VOC XXX. The catalog pointer is there and pointing to the correct program. But still the system crashes with being unable to load XXX.

Then a vestigial memory oozed from the nether regions of our consciousness. In AREV, VOC and MD are synonyms. In AREV, CATALOG writes to the VOC file. In OI VOC and MD are two separate files. One used for AREV32 and one used for CTO/OI. 

So EDIT MD XXX

New Record.

COPY VOC XXX TO:(MD

and all was fixed,  nothing, fortunately, to do with the upgrade.

Having been bitten by one missing VOC record we thought discretion was the better part of valour, and copied over all missing MD entries from VOC. All should be good yes? So we logged out of the system.

Next time we logged in, disaster struck. ENG0711 RLIST. Cannot start engine... 

Once again it's LH logs to the rescue. We tried to start OI, and again it failed. But from the log we could see that the system wanted to load RLIST. So it went to the Global MD file and looked for an RLIST entry. Regretfully it found one pointing to the AREV RLIST which doesn't take parameters - it parses @SENTENCE - and called it instead of OI RLIST.

Armed with this knowledge we could restore the Global MD files and sanity was restored.

Almost happy with the solution we hit a realisation. ALL of the catalog pointers in the client's app are in VOC - not MD. So why isn't the system falling over left right and centre? 

The answer lies in OI's RTP27 program loader optimisations. 

When OI knows that it's running as a native windows app, RTP27 first looks in the Global MD for a pointer to the program in question, and if finding a pointer, executes that program, and if failing follows the inheritance chain looking for the program in SYSOBJ. In theory RTP27 then checks your VOC file, but this currently (10.2.3) tends not to work.

When OI knows that it's running as AREVNN, it first checks VOC, then MD, then looks for the program. 

The issue here was that despite the fact we were running AREV32, the Export Window was running as a native window and so skipped the VOC step. If only we'd taken our own advice from over a decade ago...

In summation, RTP27 loads programs in the following order    

OIMDSYSOBJVOC
AREVNNVOCSYSOBJMD
CTOMDSYSOBJVOC
By Sprezz | Tuesday, 7 January 2025 08:45 | 0 Comments

Some seven years ago, REVDEX documented the then newly introduced concept of #region to allow folding of code. We find this to be incredibly useful when working on large commuter programs as it allows easy navigation between code sections. There's no point in reproducing the linked article, so briefly, you can bracket sections of code between #region regionname and #endregion regionname and then fold that section of code.

Here's an example of one of our commuter routines with the various code sections "regioned".


I'm able to see at a glance what a large program does. If I want to look at the supportingRoutines I can just dropdown the "Find label..." combo and select that option


The editor then takes me to the line number where that region begins.

However, there is a minor caveat to be aware of when we're cutting and pasting collapsed regions.

In the following pseudo-code (with a nod to Jackson Structured Programming throwback for those as ancient as myself) we've simply placed an individual subroutine within a region. That's not normally a real life scenario. You use regions to group logically connected groups of code. Of course you may choose to do this if you wish to be able to collapse individual subroutines.


and collapse it to make cutting and pasting easier


Let's decide that we want to move the process region to after the wrapup region, so we select the "#region process"


and Ctrl-X to cut the region


then move to the end of the program and press return and Ctrl-V to paste the region


All looks sort of good. But let's expand everything again and see where we are



It has brought across the process region line but inserted it within the wrapup region.

So let's revisit and this time change the selection


Note that we have now selected to the beginning of the next line - you can see the cursor. Cut, move down and paste and the expansion now looks like this


the process region has been inserted within the wrapup region, not after.

To fix this what we have to do is expand the wrapup region before doing the paste, like so


and after pasting and expanding we now have what we originally wanted.


Well worth paying a little attention to detail. 

By Sprezz | Friday, 15 November 2024 14:52 | 0 Comments

An interesting query made its way into the office this week. Our client wanted to be able to validate data using Regular Expressions (REGEXP) for Email validation (^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$ if you must know) and so unsurprisingly searched the documentation for REGEXP and came across its usage in RTI_JSON. This led to a series of mutual misunderstandings which led to a far greater understanding of what JSON actually is (see footnote) but still didn't help in actually validating using a REGEXP.

Fortunately, we have resources in the office who were able to advise that what we wanted to achieve was actually relatively simple using the OI ActiveX Scripting Host. This is something that is not yet "officially" documented in the product - but like quite a few things in OI, there is profuse documentation in the Equates. In version 10 this has been handily encapsulated for us the the RTI_AXSH function - the ActiveX Scripting Host.  ActiveX Scripting and Active Scripting are the same thing.  The X was dropped over the years. 

But first - what actually is a Scripting Host?

An Active Scripting Host is a technology used in Windows to support component-based scripting. It allows different scripting engines to be integrated into applications. This enables the execution of scripts written in various languages like VBScript, JScript (Microsoft’s implementation of JavaScript), Python, Perl and others. If we have a task we wish to execute that is better suited to say, JScript - performing a REGEXP for example - we can use RTI_AXSH to do this for us. 

To show how easy it is, we'll first provide a small sample program to do this, then explain the program and finally document the routine.

The Code

compile function test_revaxsh_regexp( void )

   $insert rti_AXSH_Equates
   $insert rti_SSP_Equates
   $insert logical

   call set_Status( FALSE$ )

   errStat = SETSTAT_OK$
   errText = ""
   hAXSH  = 0
   retVal = ""

   code =   're = new RegExp( "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" );'
   code :=  'function testRE( str ) {'
   code := '   return re.test( str );'
   code := '}'

   createParams = ""
   createParams<REVAXSH_CREATE_POS_LANGUAGE$> = "JScript"

   hAXSH = rti_AXSH( REVAXSH_MTD_CREATE$, createParams )

   errStat = get_Status( errText )

   if errStat then
      return ""
   end

   call rti_AXSH( REVAXSH_MTD_ADDCODE$, hAXSH, code )

   errStat = get_Status( errText )

   if errStat then
      return ""
   end

   string = "amcauley@sprezzatura"

   if get_Status() else
      retVal = rti_AXSH( REVAXSH_MTD_RUNEX$, hAXSH, 'testRE', string )
      errStat = get_Status( errText )
   end

   if ( hAXSH ) then
      call rti_AXSH( REVAXSH_MTD_DESTROY$, hAXSH )
   end

   if errStat then
      debug
   End

return retVal

The Explanation

The first thing that we need to do is insert RTI_AXSH_Equates. Once this has been done, we construct some simple JScript code in a variable called code. For those not fluent in JScript it's probably worth explaining how the code operates.

We start by create a global variable called "re". Think of it as being like labelled common. It'll be there until the scripting host is destroyed. Loading the code instantiates the re object as a REGEXP type.

We then define a JScript function called testRE so that we can call it from within Basic+.

We initialise our create parameter to tell OI which scripting language to use and then use the CREATE method to initialise our Script Hosting. Now that we have a container to put our code in, we can add it using the ADDCODE method. Assuming no errors we then use the RUNEX method, telling it the name of the function in our code we wish to execute (testRE). We now have the result we want to do with as we will.

Finally in an act of polite housekeeping we tell the system to DESTROY the scripting host we created so as not to leak resources.

We hope that this information makes the Scripting Host a little less scary!

The Documentation - taken from RTI_AXHS_EQUATES.

The RTI_AXSH routine provides a set of methods for interacting with the ActiveX Scripting Host. Below is a detailed guide on how to use these methods, including their parameters, return values, and error handling.

The following methods are supported

MethodDescription
CREATECreates an ActiveX scripting host instance and returns its handle.
DESTROYDestroys a script host instance.
GETPROPReturns the value of a specified property.
SETPROPSets the value of a specified property.
ADDCODEAdds a block of code to the scripting host.
EVALEvaluates a statement and returns the result.
EXECUTEExecutes one or more statements (no result is returned).
RUNExecutes a specific script function and returns the result (if any).
GETENGINESReturns a list of ActiveX script engines installed on the workstation.
ADDOBJECTAdds a global named object to the scripting host.
RUNEXExecutes a specific script function and returns the result (if any), allowing the script function parameters to be passed as separate arguments.

CREATE

Creates an ActiveX scripting host instance and returns it's handle.

The instance is initialised with the following information:

    <1> Script Language (required)
    <2> AllowUI (1/0)
    <3> hwndSite
    <4> Timeout in milliseconds (-1 == no Timeout, otherwise > 0)

 e.g.

    createParam = "JScript" : @fm : TRUE$ : @fm : @@window->handle
    hAXSH = rti_AXSH( REVAXSH_MTD_CREATE$, createParam )


InputcreateParam : an @fm delimited dynamic array as specified above
ReturnsThe handle of the new AXSH instance of successful, otherwise "0".
ErrorsAll errors are returned via Set_Status()

DESTROY

Destroys a script host instance. 

    call rti_AXSH( REVAXSH_MTD_DESTROY$, hAXSH )

InputhAXSH : Handle of the scripting host to destroy             [required]
ErrorsAll errors are returned via Set_Status()

GETPROP

Returns the value of a specified property

 e.g.

    propName = REVAXSH_PROP_LANGUAGE$
    language = rti_AXSH( REVAXSH_MTD_GETPROP$, hAXSH, propName  )

InputhAXSH     : Handle of the scripting host to access         [required]
InputpropName : Name of the property to access                   [required]
ReturnsThe property value
ErrorsAll errors are returned via Set_Status()

The supported properties are "Language", "Allow UI", "SiteHnd" and "Timeout"
ANY DEFAULTS CARL?

SETPROP


Sets the value of a specified property

 e.g.
 
    propName  = REVAXSH_PROP_ALLOWUI$
    propValue = FALSE$ 
    call rti_AXSH( REVAXSH_MTD_SETPROP$, hAXSH, propName, propValue )

InputhAXSH     : Handle of the scripting host to access           [required]
InputpropName : Name of the property to access                     [required]
InputpropValue : New property value to set.
ErrorsAll errors are returned via Set_Status()

ADDCODE


Adds a block of code to the scripting host (Any immediate code will be executed as normal)

 e.g.
 
    scriptCode = "function add( a, b ) { return a + b; }"
    call rti_AXSH( REVAXSH_MTD_ADDCODE$, hAXSH, scriptCode )

InputhAXSH     : Handle of the scripting host to access           [required]
InputscriptCode : Code to add.                                                  [required]
ErrorsAll errors are returned via Set_Status()
   

EVAL


Evaluates a statement and returns the result.

 e.g.
 
    statement = "3+10;"
    retVal = rti_AXSH( REVAXSH_MTD_EVAL$, hAXSH, statement )

InputhAXSH     : Handle of the scripting host to access           [required]
Inputstatement  : Code to evaluate.                                           [required]
ReturnsThe result of the evaluation.
ErrorsAll errors are returned via Set_Status()

EXECUTE


Executes one or more statements (no result is returned)

 e.g.
 
    scriptCode = "function add10( a, b ) { return a + 10; }; add10( 3 );"
    call rti_AXSH( REVAXSH_MTD_EXECUTE$, hAXSH, scriptCode )

InputhAXSH     : Handle of the scripting host to access           [required]
Inputstatement  : Code to execute.                                             [required]
ErrorsAll errors are returned via Set_Status()

RUN


Executes a specific script function and returns the result (if any).

 e.g.
 
    scriptCode = "function add( a, b ) { return a + b; }"
    call rti_AXSH( REVAXSH_MTD_ADDCODE$, hAXSH, scriptCode )

    method = "add"
    args   = 3 : @rm : 4
    retVal = rti_AXSH( REVAXSH_MTD_RUN$, hAXSH, method, args )

ReturnsReturn value from the function (if any)
ErrorsAll errors are returned via Set_Status()

GETENGINES


Returns a list of ActiveX script engines installed on the workstation. 

 e.g.
 
   engineList = rti_AXSH( REVAXSH_MTD_GETENGINES$ )

ReturnsAn @fm delimited list of installed engines.
ErrorsAll errors are returned via Set_Status()

ADDOBJECT


Adds a global named object to the scripting host

 e.g.
 
    objName = "MyXmlDoc"
    objDoc  = OleCreateInstance( "Msxml2.DOMDocument" )
    call rti_AXSH( REVAXSH_MTD_ADDOBJECT$, hAXSH, objName, objDoc )


InputhAXSH   : Handle of the scripting host to access               [required]
InputobjName : "Script name" of the object being added           [required]
InputoleVar     : OLE object being added                                    [required]
ErrorsAll errors are returned via Set_Status()

RUNEX

Executes a specific script function and returns the result (if any), allowing the script function parameters to be passed as separate arguments (This method is the only method that can pass OLE objects to a script function)

The maximum number of parameters that can be passed is 10.  Note that this method  stops looking for script function arguments when it encounters the first unassigned one.

 e.g.

    scriptCode = "function add( a, b ) { return a + b; }"
    call rti_AXSH( REVAXSH_MTD_ADDCODE$, hAXSH, scriptCode )

    method = "add"
    arg1   = 3
    arg2   = 4
    retVal = rti_AXSH( REVAXSH_MTD_RUN$, hAXSH, method, arg1, arg2 )


InputhAXSH   : Handle of the scripting host to access               [required]
Inputmethod    : name of the function to execute                        [required]
Inputarg1         : First parameter to pass to the function
Inputarg2         : Second parameter to pass to the function
Inputarg3         : Third parameter to pass to the function
Inputarg4         : Fourth parameter to pass to the function
Inputarg5         : Fifth parameter to pass to the function
Inputarg6         : Sixth parameter to pass to the function
Inputarg7         : Seventh parameter to pass to the function
Inputarg8         : Eighth parameter to pass to the function
Inputarg9         : Ninth parameter to pass to the function
Inputarg10       : Tenth parameter to pass to the function
ErrorsAll errors are returned via Set_Status()


A note relating to all scripting errors

 The Execute, AddCode, Eval and Run methods can all return parsing and execution errors from the scripting engine. In this case an @svm delimited list of error information is returned with the following structure:

    <0,0,1> Error Source
    <0,0,2> Error Description
    <0,0,3> LineText
    <0,0,4> LineNumber
    <0,0,5> CharPos
    <0,0,6> ContextID


Footnote: 

JSON (JavaScript Obect Notation) is a way of serializing JS variables/objects to a human-friendly string format so they can be saved and reloaded. It is not intended as a way to create JS objects and then call their methods and such... despite what the author thought


Pro Tip

CoPilot understands RegExp well. It suggests replacing the REGEXP we started with with "^[\w.%+-]+@[\w.-]+\\.\w{2,}$". 



 

Pixel
Pixel Footer R1 C1 Pixel
Pixel
Pixel
Pixel