Building "The Halls of ZK" without BLISS


Topic author
jhamby
Contributor
Posts: 22
Joined: Wed Oct 04, 2023 8:59 pm
Reputation: 0
Status: Offline

Building "The Halls of ZK" without BLISS

Post by jhamby » Wed Nov 01, 2023 3:52 pm

So I just learned about a text adventure from 1985, "The Halls of ZK", which is written in VMS Pascal and was only available as a VAX binary until somewhat recently, when original sources were discovered and updated to generate MACRO-32 tables instead of VAX object files. The current source is on GitHub:

https://github.com/tesneddon/zk

I ran into one problem building it on x86: the lack of public availability of a BLISS compiler. There's exactly one source file written in BLISS (converted from MACRO-32, the version I would've preferred to have):

Code: Select all

        %TITLE 'IFC$RTL_MACRO - IFC Run-Time System code in BLISS'

MODULE IFC$RTL_BLISS(IDENT = 'X01.00-00'
%IF %BLISS(BLISS32V) %THEN
                     ,ADDRESSING_MODE(EXTERNAL = GENERAL)
%FI
                    ) =
BEGIN
!
! Edit History:
! 13-Sep-2009  TES   Converted the MACRO-32 module to BLISS.
!
!

GLOBAL ROUTINE IFC$MESSAGE : NOVALUE =
BEGIN
    EXTERNAL ROUTINE
        IFC$MESSAGE_LIST : NOVALUE;

    BUILTIN
        ARGPTR;

    IFC$MESSAGE_LIST(ARGPTR());
END;

GLOBAL ROUTINE IFC$MESSAGE_INDENT : NOVALUE =
BEGIN
    EXTERNAL ROUTINE
        IFC$MESSAGE_INDENT_LIST : NOVALUE;

    BUILTIN
        ARGPTR;

    IFC$MESSAGE_INDENT_LIST(ARGPTR());
END;

END
ELUDOM
The only thing the wrapper routines are doing is bypassing Pascal's argument type checking so that the Pascal callers can pass a variable number of arguments, printf()-style, and the Pascal implementation sees a list of unsigned ints with the first one being the parameter count. Here's how the BLISS routines are defined to Pascal, in ifc/ifcrtl_def.pas:

Code: Select all

[asynchronous, external(ifc$message)] function $message(
        %immed message_codes : [list] unsigned) : unsigned;
        extern;

[asynchronous, external(ifc$message_indent)] function $message_indent(
        %immed message_codes : [list] unsigned) : unsigned;
        extern;
The actual Pascal implementation in ifc/ifcrtl_screen.pas looks like:

Code: Select all

type ...
        $message_args = array[0..30] of integer;

[global] function ifc$message_indent_list(
        ap : $message_args) : unsigned;
var     i, fao_count, column : integer;
        message_code : unsigned;
begin
...
end;

[global] function ifc$message_list(
        var ap : $message_args) : unsigned;
var     i, fao_count : integer;
        message_code : unsigned;
begin
...
end;
The OpenVMS Calling Standard, 5.7.5.2. OpenVMS Variable Argument Lists, says there's a synthetic emulation of the VAX argument block layout for BLISS and Pascal programs that use ARGPTR(), the Pascal list syntax, or the va_count() extension to C, and presumably the BLISS compiler generates the appropriate conversion code for x86-64. In reality, most of the arguments are being passed by register, and not in memory so it's not efficient to emulate the VAX layout.

I tried to write some C functions to manually loop through va_list and copy the va_count and parameters into a list to pass to the implementation functions, and amazingly got ifc$message() to mostly work, but the indented messages, which I assume are going through ifc$message_indent(), aren't printing. So when I try to read objects in the game, it says "The sign reads:" and then nothing.

What would be the preferred way to do this Pascal variable argument hackery in an x86-friendly way? Can the wrappers be written efficiently in C with compiler intrinsics? Is the original MACRO-32 version available, and if so, would that actually be the shortest way to do the wrapper? Or can someone with expertise in this wizardry supply such a wrapper from the BLISS version? Thanks!


Topic author
jhamby
Contributor
Posts: 22
Joined: Wed Oct 04, 2023 8:59 pm
Reputation: 0
Status: Offline

Re: Building "The Halls of ZK" without BLISS

Post by jhamby » Wed Nov 08, 2023 8:25 pm

I ran into a couple of problems trying to fix this. First, my knowledge of Ada has led me astray. I thought it would be easy to create array slices using the ".." operator, but you can only do that with strings and no other types of arrays, apparently:

Code: Select all

PASCAL/NOLIST/OBJECT=OBJ$:IFC$RTL_SCREEN.OBJ IFCRTL_SCREEN.PAS

                        fao_block := fao_block[line_fao_count+1..upper(fao_block)];
..............................................^
%PASCAL-E-STROPNDREQ, String (CHAR, PACKED ARRAY [1..n] OF CHAR (n<=65535), VARYING, or STRING) operand required
at line number 333 in file DISK$X86DATA:[home.jhamby.Projects.vms-halls-of-zk.ifc]ifcrtl_screen.pas;176

                                        fao_codes[1..fao_count]);
.................................................^
%PASCAL-E-STROPNDREQ, String (CHAR, PACKED ARRAY [1..n] OF CHAR (n<=65535), VARYING, or STRING) operand required
at line number 367 in file DISK$X86DATA:[home.jhamby.Projects.vms-halls-of-zk.ifc]ifcrtl_screen.pas;176
I spent some time looking for a SUBSTR() function that would work on integer arrays, but no such luck. The challenge is I need to take variable numbers of arguments from the caller and pass them, along with an $FAO format string that's looked up in the binary data tables generated by a utility program, to $FAOL.

Unless there's some way to pass a subset of the list arguments along to another caller, I'm going to have to manually copy them into a local integer array, then pass that array to the function that calls $faol. And I can't rely on the ability to construct subsets of the integer array that I'd been led by the syntax to believe was there for more than strings.

Calling $fao instead of $faol doesn't look possible either, since the caller doesn't know in advance how many fao params it will need to pass for any call, and I know it can be up to 7.

Added in 3 hours 24 minutes 24 seconds:
Persistence paid off, and I did get the game working without the ugly hack that led me to start this thread. The trick is that the Pascal bindings to the system routines want you to pass varying arrays of char, possibly with "%descr" in front, and not pointers to char arrays. The same thing for the $faol parameters: it needs an array of integer and not a pointer to one.

The Pascal reference manual isn't clear on the details, but Appendix A on data representation talks about how arrays and other types have a "control" and a "data" portion, and that Pascal programs can't access the control portion. So all the functions worked with the VAX argument pointer passing hack because that enabled the code to get at a pointer to the "real" array, and also their definitions of the system services had different parameter types than the real ones. Once I switched to use LIST and Argument, I could only get pointers that didn't work, or at least not directly, so I had to make temp copies.

I've pushed that change to a "jhamby/modernize" branch of my repo, where I'm doing some other cleanups.

If someone knows of ways to pass all the arguments to sys$faol from a variable-argument-count call without doing the short data copies that I had to do to make it work, I'd be curious to hear it. As a side note, the files messing up git are being generated by helper programs, and they're not Stream_LF, so I should be able to fix that issue by changing the file creation arguments.

It's too bad DEC Ada hasn't been ported. I would be happy to work on porting some version of Ada to x86-64 (presumably GNAT) if there's a demand and a budget to pay me. Working on all this Pascal code has got me in an Ada frame of mind.

User avatar

imiller
Master
Posts: 147
Joined: Fri Jun 28, 2019 8:45 am
Reputation: 0
Location: South Tyneside, UK
Status: Offline
Contact:

Re: Building "The Halls of ZK" without BLISS

Post by imiller » Thu Nov 09, 2023 5:47 am

re ADA, see http://www.vmsadaall.org/ or email contact at vmsadaall
Last edited by imiller on Thu Nov 09, 2023 5:48 am, edited 1 time in total.
Ian Miller
[ personal opinion only. usual disclaimers apply. Do not taunt happy fun ball ].


Topic author
jhamby
Contributor
Posts: 22
Joined: Wed Oct 04, 2023 8:59 pm
Reputation: 0
Status: Offline

Re: Building "The Halls of ZK" without BLISS

Post by jhamby » Thu Nov 09, 2023 12:58 pm

That's an interesting link, thanks! I knew the GNAT compiler had already been ported to Itanium, but I could never figure out where to obtain a free copy of it. Starting from the Itanium port, an x86-64 port should be quite straightforward, at least for someone with in-depth knowledge of toolchains and internals (like myself). I'd love to work on that, as a paid contract.

I spent some time thinking about my memories of DEC Ada as a student in the 1990s, and comparing it to today's Ada, and I think the biggest weaknesses of DEC Ada were limitations of Ada 83. DEC Ada's development environment was quite nice, although it's funny to me that they had the chutzpah to charge an extra license fee for the "Professional Development" option that turned on smart recompilation of dependencies and block-level caching of the library files. But I'm sure the product was expensive to develop, considering how big of a language Ada is, and they needed to recoup their R&D expenses somehow.

As a student, reading text files was always confusing because of Ada's insistence on line terminators, page terminators, and file terminators. So there are "End_Of_Line", "End_Of_Page", and "End_Of_File" checks, and you won't see the last one until you've seen the other two. That's a language issue, not VMS.

The more fundamental language-level annoyance was the lack of variable-length string support. Until Ada 95 added Ada.Strings.Unbounded, you had to allocate the max size of the char array manually and then keep track of the string length separately. So there's now a Get_Line function that returns a String (char array) directly, but in Ada 83, you had to call GetLine(Str, Len), passing the pre-allocated String and a variable to put the number of chars read, as "out" parameters.

The reason the Get_Line function wouldn't have been useful in Ada 83 is that without an unbounded string type, and without knowing in advance how big the returned String would be, it's hard to copy the result to a variable to use it. While that standard library limitation was annoying, Ada's support for arrays in general is quite good, and it has strong array slicing features that, as I said, I just assumed (wrongly) that Pascal would have as well.

The Ada bindings to OpenVMS had to remove the "$", change everything to a procedure returning the condition value as the first "out" parameter, and make a few other changes to accommodate the language. As with Pascal, it has built-in handling for descriptors, but can only handle Class_S static-length strings, due to the lack of a varying-length string type. Curiously, it looks like the Pascal bindings also only map Class_S descriptors, and for writing a string result of a variable length, the mapping looks ugly, like "%REF CHARBUFF : [UNSAFE] ARRAY [$l3..$u3:INTEGER] OF $UBYTE := %IMMED 0;".

Post Reply