Hooking in BASIC+ – Using Different Bait

OI 10 Logo Transparent2

We hope you are enjoying our series on the SRP PreCompiler. Don’t worry, we’ll have more tasty goodness to serve you in just a few days. However, we wanted to take a quick detour and revisit our recent article on hooking, a technique that allows developers a way to intercept calls to stored procedures (including system stored procedures). If you are unfamiliar with this concept then please read our earlier article before proceeding. While the method we discussed has worked well for a very long time, it still leaves a footprint in the environment that is arguably intrusive. If this were only being done in a pure test environment that might be okay. But what if there is a need to add a hook within a mission critical production environment? Perhaps you want the hook to be temporary or only valid for your session. Even if it is only minimal, leaving behind permanent hooking routines bears some risk that something could go wrong and break the application. If only there was a way of hooking without resorting to renaming the original object record and replacing it with you own…

Enter the MD Table

Many of our readers know that OpenInsight (as well as Revelation and ARev) owe their heritage to PICK and various spin-off flavors known collectively as MultiValue databases. Revelation Software products have always marched to a slightly different drum than the other vendors. The lack of an MD (or Master Dictionary) table is one of them. In traditional MultiValue databases, the MD table serves a similar purpose as the VOC table for Revelation and ARev. Chief among these purposes is its role as a pointer system. Various items that were important to the application would be cataloged in the MD table (again, the same as VOC) and this is how the system could locate volumes, tables, procedures, etc., when they were required.

In many ways, the OpenInsight System Repository (SYSREPOS table), along with a few other system tables, serves the same purpose as VOC. This has been offered as one of the reasons for why VOC was never introduced with OpenInsight (that…and the fervent insistence that OpenInsight is not ARev for Windows!) However, while the repository-based storage system offers some rather nice management features, it also removes some of the flexibility that we had with VOC (or MD as with other MultiValue databases).

In 2005, Revelation introduced a new feature of OpenInsight: Character to OpenInsight (CTO). This tool was designed to help traditional (aka green screen) MultiValue applications bridge into OpenInsight. With CTO, developers could create a virtual copy of the MultiValue application (including the character-based UI), but under the hood it is running pure OpenInsight with access to all of its modern wonders.

Okay…so what does CTO have to do with an article on BASIC+ hooking? The answer lies in the introduction of the MD table, which had previously never existed. Chances are that many OpenInsight developers aren’t even aware of its existence…even fewer know how it can be used for OpenInsight’s purposes.

Mind Your Ps and Qs

The Character to OpenInsight Reference Guide is a good place to learn about the usefulness of the MD table. For this article we will focus on Procedure Pointers (P-pointers). Table Pointers, aka Q-pointers, as well as a few others can be reviewed in the aforementioned reference guide.

Procedure Pointers are simple records stored in the MD table with the following layout:

<0> Name of the original stored procedure.

<1> P <- Always the letter "P"
<2>
<3>
<4>
<5> Table where the custom stored procedure object code lives.
<6> Name of the custom stored procedure.

Using the previous example of hooking into the Editor++ commuter module, our P-pointer would look like this:

<0> RTI_EDITOR_EVENTS
 
<1> P
<2>
<3>
<4>
<5> SYSOBJ
<6> HOOK_RTI_EDITOR_EVENTS

Revelation updated the program loader (RTP27) to look first in the MD table for a Procedure Pointer record. If one exists, it loads the referenced object code from the indicated table and stored procedure name. Thus, the original object code remains perfectly intact within the SYSOBJ table. OpenInsight now calls your code instead. Did you catch that it is possible to store your object code anywhere you want? We are much closer to achieving ARev for Windows than ever before.

Bait and Switch

When creating a program hook, the usual practice is to allow the original stored procedure to still execute (as opposed to completely replacing its functionality). In our previous article, we showed how easy this is by simply copying the original object code to a something else and then calling it directly using its new name from within the hook routine. Using the MD approach, this could still be done but it is not optimal. Remember, we are trying to avoid leaving behind a footprint. Also, what if the environment got upgraded? The original object code could be updated but you might forget to copy it again. Thus, your hook could be calling outdated code.

What’s good for the goose is good for the gander. Whatever that means to you, in this context it means that if we can use an MD Procedure Pointer to route a call to our custom hook object code, then we can do the same thing to return the call back to the original object code. We just need to create a second pointer like so:

<0> RTI_EDITOR_EVENTS_ORIG
 
<1> P
<2>
<3>
<4>
<5> SYSOBJ
<6> RTI_EDITOR_EVENTS

Our hooking procedure simply needs to call RTI_EDITOR_EVENTS_ORIG. Remember, there is no $RTI_EDITOR_EVENTS_ORIG record in SYSOBJ. RTP27 nicely loads the real $RTI_EDITOR_EVENTS while our code thinks it is calling something else.

Don’t Rock the Boat Baby

So how does one safely implement a Procedure Pointer in a multi-user environment and avoid the risk of regular users tapping into our hook routine? One approach is to write a routine that writes a P-pointer record to the MD table, call RTP27 to reload the object code, and then promptly delete the P-pointer. This will likely be safe enough, but it is not 100% safe. In a highly active system, there is always the possibility that an end user will trigger the Procedure Pointer before it gets deleted.

An alternative approach is for the developer to make use of a custom MD table. Only this custom MD table would contain the developer Procedure Pointers. However, there is a caveat. RTP27 caches the handle to the original MD table. Even if the developer switches over to another MD table, RTP27 will not make use of it. Fortunately, this problem can be resolved by running a small routine that updates the same global common that RTP27 uses:

Subroutine Refresh_MD_Handle(Void)

// Assumes the new MD table has already been attached.

Common /RTP27COMMON/ MD.FL@, LAST.FL@, HAVE.MDFL@

Open 'MD' to MD.FL@ then
    HAVE.MDFL@ = 1
end else
    MD.FL@     = ''
    HAVE.MDFL@ = 0
end

Return

Catching the Big One

This relatively non-intrusive technique for BASIC+ hooking has great value for tool vendors like SRP. For example, with a Procedure Pointer we can easily track when entities are being updated via the Repository COMPILE method and then feed this information into our toolkit. This now gives us the ability to interact with OpenInsight in real-time while development in other tools is occurring. In fact, we have a major release of one of our most popular tools come out very soon that is taking full advantage of P-Pointers. Stay tuned for more information.

6 Responses to Hooking in BASIC+ – Using Different Bait

  • Barry Stevens says:

    Thanks, the MD table entry works well, I used it to replace http_json_services with my changes testing one. It is a godsend.

  • Bob Carten says:

    Powerful stuff.

    >> call RTP27 to reload the object code, and then promptly delete the P-pointer. This will likely be safe enough, but it is not 100% safe.

    I believe a GARBAGECOLLECT statement could release your custom version. Without the MD entry RTP27 will load the original on the next call. The custom MD suggestion is more stable. You could put an MFS on MD or hack the handle in RTP27COMMON to inject an MFS which services reads from an in-memory table else falls through to MD. The same mfs could block writes to MD unless the user is a member of an elevated group.

    • Don Bakke says:

      Very cool ideas Bob. I suppose my only hesitation with GarbageCollect is that it forces everything to refresh. Hey…since you are here, I have a question. πŸ™‚ Should I expect the same routine that calls GarbageCollect itself not to be refreshed? That has been my experience, and I think it makes sense to avoid a crashing, but I wanted to get an official confirmation on this.

  • Seems the MD is in the GLOBAL application. Thus, when I made my MD rows in the CUSTAPP and placed my “hook” stproc in that app, I could no longer use the SRP Editor in another application.

    So, I plan to place my “hook” stproc in SYSPROG and have application-specific stproc calls.

    • Don Bakke says:

      Yes, MD is in the GLOBAL database and it is normally attached by all other applications. Thus, if your hooking procedure is keyed to a local application rather than the SYSPROG application, it could have undesirable results. There *are* ways to make this work, but putting your hook in SYSPROG would probably be the easiest solution.

  • I’m using this technique to great effect for code portability between OI9 and OI10 πŸ™‚ – at least in our development system. However it seems that RTP27COMMON is not getting initialised in our deployed systems for some reason, even though MD is attached, and the redirected MD calls cause SYS1000 errors – until Refresh_MD_Handle() (as above) is called explicitly. It seems our deployed system are missing some OI initialisation steps or configuration..

    Do you know what process initialises RTP27COMMON on logon? Is it dependant on any CTO or AREV32 configuration?

Leave a Reply to Don BakkeCancel reply