1 Introduction
2 Ground Rules

Building a File System
3 File Systems
4 File Content Data Structure
5 Allocation Cluster Manager
6 Exceptions and Emancipation
7 Base Classes, Testing, and More
8 File Meta Data
9 Native File Class
10 Our File System
11 Allocation Table
12 File System Support Code
13 Initializing the File System
14 Contiguous Files
15 Rebuilding the File System
16 Native File System Support Methods
17 Lookups, Wildcards, and Unicode, Oh My
18 Finishing the File System Class

The Init Program
19 Hardware Abstraction and UOS Architecture
20 Init Command Mode
21 Using Our File System
22 Hardware and Device Lists
23 Fun with Stores: Partitions
24 Fun with Stores: RAID
25 Fun with Stores: RAM Disks
26 Init wrap-up

The Executive
27 Overview of The Executive
28 Starting the Kernel
29 The Kernel
30 Making a Store Bootable
31 The MMC
32 The HMC
33 Loading the components
34 Using the File Processor
35 Symbols and the SSC
36 The File Processor and Device Management
37 The File Processor and File System Management
38 Finishing Executive Startup

Users and Security
39 Introduction to Users and Security
40 More Fun With Stores: File Heaps
41 File Heaps, part 2
42 SysUAF
43 TUser

Terminal I/O
45 Shells and UCL
46 UOS API, the Application Side
47 UOS API, the Executive Side
48 I/O Devices
49 Streams
50 Terminal Output Filters
51 The TTerminal Class
52 Handles
53 Putting it All Together
54 Getting Terminal Input
55 QIO
56 Cooking Terminal Input
57 Putting it all together, part 2
58 Quotas and I/O

59 UCL Basics
60 Symbol Substitution
61 Command execution
62 Command execution, part 2
63 Command Abbreviation
64 ASTs
65 Expressions, Part 1
66 Expressions, Part 2: Support code
67 Expressions, part 3: Parsing
69 Expressions, part 4: Evaluation

UCL Lexical Functions
72 TProcess updates
73 Unicode revisted
74 Lexical functions: F$CONTEXT
75 Lexical functions: F$PID
76 Lexical Functions: F$CUNITS
77 Lexical Functions: F$CVSI and F$CVUI
78 UOS Date and Time Formatting
79 Lexical Functions: F$CVTIME
81 Date/Time Contexts
83 Lexical Functions: F$DELTA_TIME
84 Lexical functions: F$DEVICE
86 Lexical functions: F$DIRECTORY
87 Lexical functions: F$EDIT and F$ELEMENT
88 Lexical functions: F$ENVIRONMENT
90 Lexical functions: F$EXTRACT and F$IDENTIFIER
92 LIB_FAO and LIB_FAOL, part 2
93 Lexical functions: F$FAO
94 File Processing Structures
95 Lexical functions: F$FILE_ATTRIBUTES
97 Lexical functions: F$GETDVI
98 Parse_GetDVI
99 GetDVI
100 GetDVI, part 2
101 GetDVI, part 3
102 Lexical functions: F$GETJPI


Download sources
Download binaries

UCL Command execution, part 2

In this article, we continue our discussion of UCL command processing in the Process procedure. We left off with the setting up of the parser and checking for error conditions at the beginning of our processing loop. UCL command lines consist of assignments (setting the value of a symbol), comments, labels, and commands. So, let's handle these conditions.

            Sym := Edit( Get_Token, 512 ) ;
            if( Sym = '' ) then // All done
                exit ;
            end ;
We get the next (or first) token from the parser and put it in the Sym variable. The reason the variable is named Sym is that quite often the first token encountered in a command line is a symbol. It may turn out not to be one, but it seemed a better name than just using S. If there is no token, that means that the parser has exhausted all of the tokens in the line - in other words, we've reached the end of the command - and this is one of the ways in which we exit the loop.

            if( Sym = '@' ) then
                Sym := Parser.Grab_Line ;
                if( Sym = '' ) then
                    Output_Line( RH_SysError, '%UCL-W-NULFIL, missing or invalid file specification - respecify'  ) ;
                    exit ;
                end ;
                if( Process_At ) then
                    exit ;
                end ;
            end else
At this point, we know we have a token and it is the start of a command. So we see if the token is an at-sign (@). This indicates to run another UCL command file. In that case, we assume that the rest of the line is the file specification of the command file, so we use Parser.Grab_Line to get it. If the file specification is null we indicate an error and exit. Otherwise, we call the Process_At function to handle the command file. We will discuss this function in a future article. In the case that the function returns true, we exit immediately.

                // Process line...
                S := Get_Token ;
                if( S = ':' ) then // A label
                    if( Interactive ) then
                        Output_Line( RH_SysError, '%UCL-W-NOLBLS, label ignored - use only within command procedures'  ) ;
                        Output_Line( RH_SysError, '    \' + Sym + ':\'  ) ;
                    end ;
                    continue ;
                end ;
Next, if we aren't executing a command file, we grab the next token and see if it is a single colon (:). If so, this indicates a label. However, labels are not allowed in interactive mode - so in that case, we display an error and continue with our processing loop. For non-interactive mode, we ignore the label and continue.

Let's take a detour here and look at code for the the Interactive function:

var Descriptor : array[ 0..1 ] of TDVI_Descriptor ;
    Buff, BufLen : int64 ;

function Interactive : boolean ;

    Buff := 0 ;
    BufLen := 0 ;
    fillchar( Descriptor, sizeof( Descriptor ), 0 ) ;
    Descriptor[ 0 ].Buffer_Length := sizeof( Buff ) ;
    Descriptor[ 0 ].Item_Code := DVI_DEVCHAR ; // DVIDEF item
    Descriptor[ 0 ].Buffer_Address := integer( @Buff ) ;
    Descriptor[ 0 ].Return_Length_Address := integer( @BufLen ) ;
    SYS_GETDVIW( 0, RH_SysCommand, '', integer( @Descriptor ), integer( @IOSB ),
        0, 0, '', 1 ) ;
    Result := ( ( Buff and DEV_V_TRM ) <> 0 ) ; // Terminal device
end ;
To determine if the sys$command device is a terminal (ie interactive), we can call the SYS_GETDVIW system call (GETDVIW = Get device information and wait for a result). This call can return multiple pieces of device information, which can differ for each kind of device. So, the call takes a list of descriptors that indicate what information is requested and where to return it. A list of descriptors is like an array, and can be defined as one. The descriptors must exist in contiguous memory and the last descriptor should be a special terminal descriptor that lets UOS know where the descriptor lists ends. For now, we will only concern ourselves with the DVI_DEVCHAR data request, which return device characteristics for the passed handle.

First we initialize Buff and BufLen to 0. These are the variables that will receive the returned data. Next we initialize the Descriptor array to 0. This will set the second (and last) descriptor to nulls, which indicate a terminal descriptor (it also sets the first descriptor to nulls, but we immediately change those values). Then we set the first descriptor in the array to the necessary values. The Buffer_Length indicates the maximum length of the receiving buffer, the item code indicates the information we want, the buffer address indicates the address of the receiving buffer, and the return length address indicates where the actual returned data length is stored.

You might think that this is an awful lot of effort to go to in order to retrieve a simple integer value containing device flags. So why do it this way? The simple answer is: because it is the way that VMS does it. But we aren't just slavishly following VMS - there are some good reasons why VMS does it this way. First, there are dozens of different pieces of information that can be returned about a given device. If there were separate system calls for each piece, the system API would quickly become unmanageably large. Second, we've noted that calls into ring 0 have overhead associated with them and - as we will see in future articles - there are cases where a program will be asking for many pieces of information about a device. Using our approach, all of that information can be queried with a single system call into ring 0. This results in much less overhead, which translates into higher system performance. Third, one might be tempted to have a single system call that returned every possible bit of information about a device. While that might solve the first two problems, there are two reasons why this would be a bad choice: 1) The amount of data returned will be different for different types of devices and even different for difference devices of the same type. Some of this information is variable in length and it would be nigh to impossible to provide everything about a device without querying UOS for the size of the resulting data, building an image of the data in the executive so we can respond with that size, and then the call to get it, which would involve building the image of the data again. But even worse, there is some overhead associated with gathering the information about a device. The more information we return, the more overhead is involved. If we only want one or two pieces, why waste the CPU cycles on gathering data that is not going to be used by the caller? Not to mention the time to transfer much more data to the application. But even ignoring the overhead of transferring more data, we can't return a record structure from the executive to the user because the information isn't all contained in a single structure - and even if it were, that would create a coupling between the application and the specific implementation in the executive. Thus, the VMS approach has the added benefit of decoupling the internal implementation with the interface for retrieving that data. This is another example of how a little bit of extra effort and discipline can pay off in multiple ways.

Here is the definition of the TDVI_Descriptor structure:

type TDVI_Descriptor = packed record
                           Buffer_Length : cardinal ; // Length of result buffer
                           Item_Code : cardinal ; // DVIDEF item
                           Buffer_Address : int64 ; // Address of result buffer
                           Return_Length_Address : int64 ; // Where to write result length
                       end ;

Now let's look at the SYS_GETDVIW function in the Sys unit.

procedure SYS_GETDVIW( efn, Handle : int64 ; Device : string ;
    itmlst, IOSB, ASTAdr, ASTPrm : int64 ; Pathname : string ; Siz : integer ) ;

var Status : byte ;
    SysRequest : TFile_Request ;

    fillchar( SysRequest, sizeof( SysRequest ), 0 ) ;
    SysRequest.Request.Subsystem :=  UOS_Subsystem_FIP ;
    SysRequest.Request.Request := UOS_FIP_GetDVIW ;
    SysRequest.Request.Length := sizeof( SysRequest.FRB ) ;
    SysRequest.Request.Status := integer( @Status ) ;
    SysRequest.FRB.Handle := Handle ;
    SysRequest.FRB.Buffer := ItmLst ;
    SysRequest.FRB.Length := Siz ;
    SysRequest.SRB.Buffer := integer( PChar( Device ) ) ;
    SysRequest.SRB.Length := length( Device ) ;

    Call_To_Ring0( integer( @SysRequest ) ) ;
end ;
SYS_GETDVIW uses the TFile_Request structure that we introduced in article 53. A pointer to the device name, and its length, as passed along with the handle, a pointer to the descriptor list, and the number of descriptors passed. You may wonder why we pass a count of descriptors if we also have a terminating descriptor. The fact is that we can do either: UOS will stop processing descriptors if a terminating descriptor is encountered even if the passed count is larger than that. In other words, UOS will process until the terminating descriptor or until it has processed as many descriptors as we indicate - whichever happens first. It is simply good practice to always include a terminating descriptor as a guard against programming errors.

You may also wonder why we pass both a device name and a handle. In fact, we need to only pass one of them. The function allows you to reference a device by name or by handle. In the above example, we pass a null string for the device name, so that it uses the handle. If both are passed, the device name will take precedence. Note: using the handle is slightly more efficient that passing the device name ("sys$command" in this case). The device name is used when we don't have a handle to the device, but still want information about it.

Finally, let's look at the additional FiP code to respond to this call. First the new code in the API method.

                if( not Enter_Standard_FRB_Call ) then
                    exit ;
                end ;
                    Get_Device_Info( SysReqFRB.FRB.Handle, SysReqFRB.FRB.Buffer, SysReqFRB.FRB.Length, SysReqFRB.SRB ) ;
                    Exit_Standard_FRB_Call ;
                end ;
            end ;
Nothing more here than to call the Get_Device_Info with the appropriate values.

And here is the Get_Device_Info method code.

procedure TUOS_FiP.Get_Device_Info( Handle, Buffer, Length : int64 ; SRB : TSRB ) ;

var Buff : PAnsiChar ;
    Descriptor : TDVI_Descriptor ;
    Device_Name : TUOS_String ;
    Loop, Status : integer ;
    Offset : int64 ;
    PID : TPID ;
    Res : cardinal ;
    Resource : TResource ;
    Temp : Ansistring ;

    // Setup...
    if( Length = 0 ) then
        exit ;
    end ;
    PID := Kernel.PID ;
    Handle := USC.Translate_Handle( PID, Handle ) ;
    Resource := TResource( Handle ) ;
The first thing we do is check if the passed number of descriptors is 0. If so, we exit immediately to avoid doing extra work for nothing. Then we get the current PID and translate the passed handle. Note that, for now, we are ignoring the case of a device being passed by name. Then we get the resource associated with the handle.

        for Loop := 0 to Length do
            move( Buff[ Offset ], Descriptor, sizeof( Descriptor ) ) ;
                ( Descriptor.Buffer_Length = 0 )
                ( Descriptor.Buffer_Address = 0 )
                ( Descriptor.Return_Length_Address = 0 )
              ) then // End of list
                exit ;
            end ;
Now we loop through the descriptors until we reach the passed length of the descriptor list. We move the data from the user memory to a local Descriptor variable. If any of the address or length values are zero, we consider this a terminating descriptor and exit.

            case Descriptor.Item_Code of
                DVI_DEVCHAR : begin
                                  if( Descriptor.Buffer_Length > sizeof( Res ) ) then
                                      Descriptor.Buffer_Length := sizeof( Res ) ;
                                  end ;
                                  Res := 0 ;
                                  if( TFiP_File( Resource._File ) is TFiP_Terminal_File ) then
                                      Res := DEV_V_TRM ;
                                  end ;
                                  setlength( Temp, Descriptor.Buffer_Length ) ;
                                  move( Res, PChar( Temp )[ 0 ], Descriptor.Buffer_Length ) ;
                                  SRB.Buffer := Descriptor.Buffer_Address ;
                                  SRB.Length := Descriptor.Buffer_Length ;
                                  Status := Set_User_String( Kernel, PID, SRB, Temp ) ;
                                  if( Status = UE_Error ) then
                                      if( MMC.Last_Error = nil ) then
                                          Generate_Exception( UOSErr_Memory_Address_Error ) ;
                                      end ;
                                      exit ;
                                  end ;
                              end ;
            end ;
Now we check the item code (note that we are only handling DVI_DEVCHAR at present). If the passed length is greater than the size of a cardinal, we adjust it down. Res is going to be the set of flags that we return, so we set it to 0 to clear them. If the resouces is a terminal, we set the DEV_V_TRM flag. We will look at other flags in the future. When we are finished setting the flags, we set the size of Temp and copy the flags into the string. This is so we can use the Set_User_String call to copy the data back to the user. That is also the reason for setting up SRB. After the Set_User_String call, we check for an error code. If there was an error, we generate a memory error and exit.

            Offset := Offset + sizeof( Descriptor ) ; // Move to next descriptor
        end ; // for Loop := 0 to Length
            MMC.UnMap_Pages( 0, Buffer, Length * sizeof( Descriptor ) ) ;
        end ;
        MMC.Unlock_Pages( PID, Buffer, Length * sizeof( Descriptor ) ) ;
    end ;
end ; // TUOS_FiP.Get_Device_Info
Finally, we update the offset to point to the next descriptor and loop back to process it. After the loop ends, we unmap and unlock pages, as usual.

In the next article, we will continue our discussion of UCL command processing.


Copyright © 2019 by Alan Conroy. This article may be copied in whole or in part as long as this copyright is included.