While fiddling around with something, I chanced to delve a bit deeper into SMSQ/E’s PEEK family of keywords. I wandered if it would be possible to use them to solve an ancient problem.
Heres a scenario, or rather, two:
- A demo program loader, or an installation program, needs to check whether certain toolkits are already loaded on a punter’s machine or whether it needs to load them itself, or choose a different version of the compiled program to execute, – or to make a dash for the hills.
- A compiled program needs to check whether the useful QMenu FILE_SELECT$ utility is available, or whether it should use its own, simple, fall-back solution, to get a file name from the user.
Both of these scenarios could be satisfied if it were possible to check whether certain keywords are loaded in the machine or not.
The problem is easily solved in the case of compiled programs. Just devise a small machine code extension to scan the Name Table, and link it to the compiled program in the usual way. It takes a few extra bytes while the program is in use, and then goes away. A demo program launcher or an installation program could install such a toolkit for this task, but then cannot tidy the mess afterwards; the useless toolkit remains until the next reboot. No big deal, perhaps, but would it be possible to check for keywords from S*BASIC without the use of toolkits?
Looking at the examples given in the manual, PEEK(\\) looks like the man for the job.
So here was my first attempt, written in SMSQ/E SBASIC:
10 DEFine FuNction FindKey(k$) 11 LOCal n, c, o, l% 12 REMark Returns true if keyword loaded 13 REMark in this SBASIC 14 : 15 REMark Get size of Name Table 16 c = PEEK_L(\\ $1C) - PEEK_L(\\ $18) 17 FOR n = 0 TO c STEP 8 18 REMark Weed out non-keywords Types 19 IF PEEK_W(\ $18\ n) < $800: NEXT n: EXIT n 20 REMark Get offset in Name List 21 o = PEEK_W(\ $18\ n + 2) 22 REMark Skip no names 23 IF o < 0: NEXT n: EXIT n 24 REMark Get Name length 25 l% = PEEK(\ $20\ o) 26 REMark and Name 27 IF k$ == PEEK$(\ $20\ o + 1, l%): RETurn 1 28 END FOR n 29 RETurn 0 30 END DEFine FindKey
Well, that worked a treat. Quick and simple. Problem solved!
First of all QLib wasnt having any of it. It didnt like my hex and it was totally biased against SMSQ/E’s version of PEEK, prefering its own version, which doesnt understand offsets.
The other, more serious, problem is that although the program above does work in SBASIC daughter jobs, it is useless: It only shows a subset of all the available keywords. This is because each of the SBASIC daughter jobs gets its own Name Table (naturally), but in this table only keywords in the current program and any immediate commands typed at the console make it into the local Name Table. The main table is associated only with job #0, guardian of the SBASIC Stub. This stub, a construct devised to maintain a high degree of compatability with SuperBASIC, holds the pointers to the real Name Table, maintained somewhere in the Common Heap. Like SuperBASIC’s data space, this Name Table can move, but not in the same way nor for all the same reasons as SuperBASIC.
It can move when engorged with new keywords as toolkits are loaded in job#0 at boot time. It can move when lots of new names are added to the interpreter in job#0. When it needs to grow, a new, larger area is reserved for it in the Common Heap, the current values are then copied over, and finally the pointers in the SBASIC Variables area are updated, before the old Name Table heap is discarded. (It can also shrink, due to a clearout of names from interpeter#0, which will affect at least some pointers.) In other words, it will only move due to some activity going on in the job#0 interpreter.
The moving memory model employed by SuperBASIC evokes atavistic nightmares and strange nervous ticks in many a stout hobby programmer. SBASIC doesnt really have to stress so, but valiantly maintains a fascade of forgiving compatability for the benefit of ancient programs and cheeky programmers.
So back to the issue at hand: I could see now that I somehow had to scan the real table, job #0’s Name Table. The problem is there is no easy way to grab that table by the cojones and pin it down by invoking supervisor mode from SBASIC. What to do?
In the two scenarios I envisage above, any scanning of the Name Table, whether in job#0, a daughter SBASIC, or a compiled job, could be made to occur at the very start of a given program. It is highly unlikely that a large S*BASIC program would be started at exactly the same time, and if used in a boot program, nothing apart from my program’s own instructions would be happening in job#0. Although the unlikely, famously, should never be confused with the impossible, in SMSQ/E at least, it seems a pretty safe bet.
I thought Id go ahead and try it out. Then Id stress test it to see how great the issue of moving memory was to my scheme. So I added some trival tests. If the worst came to the worst and it did move, it should not have worse consequences than giving a wrong answer once in a blue moon:
10 DEFine FuNction FindKey(k$) 11 LOCal n, o, a6, c, s, l% 12 REMark Searches all keywords 13 REMark Return true on match 14 : 15 REMark Get job#0's JCB 16 s = PEEK_L(! $68! 0) 17 REMark Get job#0's a6 18 a6 = PEEK_L(s + $58) 19 : 20 REMark Some GLOBal definitions 21 sb_nmtbb = a6 + $18 22 sb_nmtbp = a6 + $1C 23 sb_nmlsb = a6 + $20 24 : 25 REMark Size of Name Table 26 c = PEEK_L(sb_nmtbp) - PEEK_L(sb_nmtbb) 27 : 28 REMark Go through list 29 FOR n = 0 TO c STEP 8 30 : 31 REMark Weed out non-keywords Types 32 IF PEEK_W(PEEK_L(sb_nmtbb) + a6 + n) < $800: NEXT n: EXIT n 33 : 34 REMark Get offset in Name List 35 o = PEEK_W(PEEK_L(sb_nmtbb) + a6 + n + 2) 36 : 37 REMark Skip no names 38 IF o <= 0: NEXT n: EXIT n 39 : 40 REMark Get Name length 41 l% = PEEK(PEEK_L(sb_nmlsb) + a6 + o) 42 IF l% > 255: NEXT n: EXIT n 43 : 44 REMark Display Name 45 IF k$ == PEEK$(PEEK_L(sb_nmlsb) + a6 + o + 1, l%): RETurn 1 46 END FOR n 47 RETurn 0 48 END DEFine ListKeys 49 : 50 PRINT FindKey("block") 51 PRINT FindKey("blox")
As it stands, its optimised for presentation rather than speed. RUNning the program in any SBASIC should print 1 and 0 to the screen, whether or no BLOCK is loaded in the local interpreter. It still wont run on anything but SBASIC, so further modification is needed to make it universal.
A lot of programmers assume that the System Variables always will be located at address $28000, as they are under Qdos on an original QL. But even on the QL it wasnt intended to be fixed at that address, what with the second screen and all. But thats how the cookie crumbled, and SBASIC has obligingly condoned it by supplying its System Variables at the same address. However, that is mainly to allow users to run old programs that cannot be altered. New programs should not rely on unintended addresses. The next generation of emulators or hardware (you wish!) may, for technical reasons, not be so accommodating. Mercifully, in a little hack nicked from Minerva, the function VER$(-2) is available in SMSQ/E too. QDOS, of course, will never change, so this switch should work on all current systems (Please let us know if you know otherwise!):
IF VER$ = 'JSL' OR VER$ = 'HBA' THEN SYSV = VER$(-2): REMark Minerva/SMSQ/E ELSE SYSV = 163840: REMark QDOS END IF
While the System Variables may not always be located in the “standard” location (at least from Minerva and onwards), once the system is up and running, their location remains fixed throughout the session, so this only needs to be checked once.
The final listing shows the completed program:
10 DEFine FuNction FindKey(k$) 11 LOCal i, n, o, a6, c, s 12 LOCal l%, k% 13 REMark GLOBal SYSV 14 : 15 REMark Get job#0's JCB 16 s = PEEK_L(PEEK_L(SYSV + 104)): REMark $68 17 REMark Get job#0's a6 18 a6 = PEEK_L(s + 88): REMark $58 19 : 20 REMark Some GLOBal definitions 21 sb_nmtbb = a6 + 24: REMark $18 22 sb_nmtbp = a6 + 28: REMark $1C 23 sb_nmlsb = a6 + 32: REMark $20 24 : 25 REMark Size of Name Table 26 c = PEEK_L(sb_nmtbp) - PEEK_L(sb_nmtbb) 27 : 28 k% = LEN(k$) 29 REMark Go through list 30 FOR n = 0 TO c STEP 8 31 : 32 REMark Weed out non-keywords: REMark $800 33 IF PEEK_W(PEEK_L(sb_nmtbb) + a6 + n) < 2048: NEXT n: EXIT n 34 : 35 REMark Get offset in Name List 36 o = PEEK_W(PEEK_L(sb_nmtbb) + a6 + n + 2) 37 : 38 REMark Skip no names 39 IF o < 0: NEXT n: EXIT n 40 : 41 REMark Get Name length 42 l% = PEEK(PEEK_L(sb_nmlsb) + a6 + o) 43 : 44 REMark For slow QLs 45 IF l% <> k%: NEXT n: EXIT n 46 REMark Compare strings 47 FOR i = 1 TO l% 48 IF NOT CHR$(PEEK(PEEK_L(sb_nmlsb) + a6 + o + i)) == k$(i): l% = 0: EXIT i 49 END FOR i 50 IF l% > 0: RETurn 1: REMark Bingo! 51 END FOR n 52 RETurn 0 53 END DEFine FindKey 54 : 55 REMark Get pointer to System Variables 56 v$ = VER$ 57 IF v$ = ‘JSL’ OR v$ = ‘HBA’ THEN 58 SYSV = VER$(-2): REMark Minerva/SMSQ/E 59 ELSE 60 REMark QDOS 61 SYSV = 163840: REMark $28000 62 END IF 63 : 64 PRINT FindKey("block") 65 PRINT FindKey("blox")
As I mentioned, theres no easy way to stop the SuperBASIC area from moving while this routine runs its course, so it is only safe to use in boot programs. It may be ok to use in a limited way in compiled programs too, if used sparingly at startup, the assumption being that its unlikely that a large S*BASIC program will be loaded at the same instance as your compiled program initialises:
10 REMark $$stak=800 20 : 100 REMark My little program 110 qmenu = FindKey("FILE_SELECT$") 120 sound = FindKey("SOUNDFILE") 130 : 140 REMark Rest of My little program goes here.. ... 710 IF sound: Play "tune": ELSE: Beeep ...
Disclaimer: I have not deliberately set out to mislead anyone, but errors may occur. There is not much published information on any of this out there, so it comes down to stitching together what scraps one can find, tracing through the source code, trying things out, and discussing with others. Please let us know how you get on with any of this, if you discover any errors, or if you have experience of other systems that dont seem to conform with the information given here. Good or bad, your feedback is welcome. Contact me through the comment facility here, or see you on QL-users or in the QL Forum!