# DOSIOCTL.PL: a Perl file of examples for ioctl() under MS-DOS,
# the previous documentation not being of the very finest.
#
# Author: John Dallman ([email protected])
# Thanks to Dan Carson ([email protected]) for getting me started
# with IOCTL, and to Larry for creating Perl in the first place.


__END__
#################################################################
#####
#       Figuring out IOCTL via Perl under DOS.
#
#################################################################
####

An extract from Len Reed's PERLDOS.MAN:

    ioctl(FILEHANDLE,FUNCTION,SCALAR)
            Implements the Unix ioctl(2) function.  You'll prob-
            ably have to say

                 require "ioctl.ph"; # probably
/usr/local/lib/perl/ioctl.ph

            first to get the correct function  definitions.   If
            ioctl.ph  doesn't  exist or doesn't have the correct
            definitions you'll have to roll your own,  based  on
            your  C  header files such as <sys/ioctl.h>.  (There
            is a perl script called h2ph  that  comes  with  the
            Unix  perl  kit which may help you in this.)  SCALAR
            will  be  read  and/or  written  depending  on   the
            FUNCTION--a  pointer  to  the string value of SCALAR
            will be passed as the third argument of  the  actual
            ioctl call.  (If SCALAR has no string value but does
            have a numeric value,  that  value  will  be  passed
            rather  than  a  pointer  to  the  string value.  To
            guarantee this to be true, add a  0  to  the  scalar
            before using it.)  The pack() and unpack() functions
            are useful for manipulating the values of structures
            used  by  ioctl().   The  following example sets the
            erase character to DEL.

                 require 'ioctl.ph';
                 $sgttyb_t = "ccccs";          # 4 chars and a
short                  if (ioctl(STDIN,$TIOCGETP,$sgttyb)) {
                      @ary = unpack($sgttyb_t,$sgttyb);
                      $ary[2] = 127;
                      $sgttyb = pack($sgttyb_t,@ary);
                      ioctl(STDIN,$TIOCSETP,$sgttyb)
                           || die "Can't ioctl: $!";
                 }

            The return value of ioctl (and fcntl) is as follows:

                 if OS returns:           perl returns:
                   -1                       undefined value
                   0                        string "0 but true"
                   anything else            that number

            Thus perl returns  true  on  success  and  false  on
            failure,  yet  you  can  still  easily determine the
            actual value returned by the operating system:

                 ($retval = ioctl(...)) || ($retval = -1);
                 printf "System returned %d\n", $retval;


            On MS-DOS, the function code of the  ioctl  function
            (the second argument) is encoded as follows:

                 The lowest nibble of the function code goes to
AL.                  The two middle nibbles go to CL.
                 The high nibble goes to CH.

            The return code is -1 in the case of an error and if
            successful:

                 for functions AL = 00, 09, 0a the value of the
register DX
                 for functions AL = 02 - 08, 0e the value of
the register AX
                 for functions AL = 01, 0b - 0f the number 0.

            The program can distiguish between the return  value
            and  the  success  of  ioctl  as  described for Unix
            above.

            Some MS-DOS ioctl functions need  a  number  as  the
            first  argument.   Provided that no other files have
            been opened the number can be obtained if  ioctl  is
            called  with  @fdnum[number]  as  the first argument
            after executing the following code:

                 @fdnum = ("STDIN", "STDOUT", "STDERR");
                 $maxdrives = 15;
                 for ($i = 3; $i < $maxdrives; $i++) {
                      open("FD$i", "nul");
                      @fdnum[$i - 1] = "FD$i";
                 }


Clarifications
==============

I had a fair amount of trouble understanding some details of this
explaination; some clarification is in order.

IOCTL calls INT21H function 44H, with register values specified
bythe parameters to the ioctl() function.  There isn't an
ioctl.phfor DOS, but at least the values that you have to use
are consistent
for all DOS systems.

DOS doesn't use data structures for IOCTL to the same extent as
UNIX: for many calls, parameters are passed in registers. Note
that all the DOS IOCTL calls are quite unlike UNIX ones: there
isn't any
portability of code that uses ioctl between DOS and UNIX.

  ioctl(FILEHANDLE,FUNCTION,SCALAR)

     FILEHANDLE is the Perl filehandle of the file to be
manipulated.      This is eventually reduced to an MS-DOS
file/device handle,
     which is passed to DOS in BX.  For drive-orientiated
     functions, trickery is required to supply a Perl filehandle
     whose MS-DOS filehandle has the right value for the desired
     drive ID - see below.

     FUNCTION is the IOCTL sub-function, passed to MS-DOS in AL.
     If a parameter is required in CX, it is passed in the upper
     nibbles of FUNCTION.

     SCALAR is a parameter for DOS, passed in DX or DS:DX. A
     pointer to its string value is passed in DS:DX; if it has
     no string value, the integer version of its numeric value
     is passed in DX.  To force this to happen, add 0 to the
     scalar before using it.  For IOCTL functions that return
data       in SCLAR, you don't have to worry about making space
- Perl
     always allocates space, although it often allocates too
much.      On most systems other than BSD UNIX, it allocates 256
bytes.

     The normal way to use IOCTL is to use pack() to create the
data       structure that MS-DOS wants in a string, and then
pass the
     string as SCALAR.  Unpack() is very handy for unpacking
such       structures whern DOS has handed them back to you.

     The ioctl function returns underfined if the return code
from       the ioctl handler is -1 (an error), otherwise DX, AX
or zero
     according to the function used (see above). Zero results
return      "0 but true".  On an error, $! is set.


"... need a number ..."
=======================

The *really* incomprehensible bit of the manpage was:

#===================================================
Some MS-DOS ioctl functions need  a  number  as  the
first  argument.   Provided that no other files have
been opened the number can be obtained if  ioctl  is
called  with  @fdnum[number]  as  the first argument
after executing the following code:

@fdnum = ("STDIN", "STDOUT", "STDERR");
$maxdrives = 15;
for ($i = 3; $i < $maxdrives; $i++) {
       open("FD$i", "nul");
       @fdnum[$i - 1] = "FD$i";
}
#===================================================

This actually means "Some MS-DOS ioctl functions need a *drive*
number as the first argument".  It is trying to say that this
can be obtained by calling ioctl() with $fdnum[drive_number] as
the first argument.  Drive numbers are 0 for the default drive,
1 for
A:, 2 for B: and so on.

This would pass a Perl filehandle, with a value chosen from
"STDIN", "STDOUT", "STDERR", "FD3", "FD4", etc...  As MS-DOS
filehandles are small positive integers, this tricks Perl
into producing the right numbers for the wrong reasons:
with no other files open, the open(FD...) calls produce
MS-DOS filehandles with the right values for the drive ID
numbers.  The default open MS-DOS files stdaux and stdprn
appear to have been closed before the Perl script starts
executing (otherwise, they'd be occupying filehandle
numbers 3 and 4).

This method seems to be bad technique:

- Opening all these files uses up FILES= file numbers, and the
 program filehandles MS-DOS has allocated to PERL.EXE.  You can
 increase FILES=, but increasing the number of program
filehandles   could only be done by PERL.EXE, via INT21H
Function 67H. This
 demands DOS 3.30, but that is no great problem.  By
 default, there are only 20 program filehandles....

There were also some errors in the Perl code: here is a version
which works correctly:

#
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++@fdnum = ("STDIN", "STDOUT", "STDERR");          # These
have handles 0..2
$maxdrives = 15;
for ($i = 3; $i < $maxdrives; $i++)             # These will
have handles
{                                               # 3..15
       open("FD$i", "nul") || die "Couldn't open FD$i : $!\n" ;
       $fdnum[$i] = "FD$i";
}
#
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++

The main error in the original code was

       @fdnum[$i - 1] = "FD$i";
                ^^^^
This meant that the STDERR value was overwritten in the array,
andthat the values put in by the $i loop were out of phase with
those inserted by the original LIST assignment to @fdnum.  We're
also addressing a single array element using @name, which is bad
practice.

Now, passing a FILEHANDLE of $fdnum[$drive] will work, where
$drive is
0 for the default drive, 1 for A:, 2 for B:, 3 for C:, etc...

Packing and Unpacking
=====================

Example: $set_itt = hex( "545C") sets $set_itt to the value of
hex("545C"), which is 21596, decimal.

The string value of $set_itt is set up at the same time, so
unpacking $set_itt unpacks the string, which contains '2', '1',
'5', '9', '6'.

Remember that the 80x86 series are ilttle-endian machines: the
less significant bytes of values are at lower memory addresses.
Thus, $in = "\n\0" is equivalent to $in = pack( "s1", 10)


#################################################################
#####
#       Examples of using MS-DOS IOCTL functions
#
#################################################################
####

#
-----------------------------------------------------------------
--# Get Device/File Data
#
# FILEHANDLE is an open filehandle on the file or device we're
# interested in.
# FUNCTION is zero.
# SCALAR is zero.
# The ioctl return value is undefined for an error, or the DX
# device info value.
#
-----------------------------------------------------------------
--

$device_data = ioctl( STDIN, 0, 0) ;
open( A_FILE, "PERL.EXE") || die "Can't access PERL.EXE: $!\n" ;
$file_data = ioctl( A_FILE, 0, 0) ;
printf( "Terminal device info = %x\nfile device info = %x\n",
                                       $device_data,
$file_data) ;

#
-----------------------------------------------------------------
--# Set Device Data
#
# FILEHANDLE is an open filehandle on the device (*not* a file)
we're # interested in.
# FUNCTION is 1
# SCALAR is the value to be written
# The return value is undefined for an error, or 0-but-true for
success.#
# This example sets STDIN to raw mode, so that input is
# not line-buffered. This is about the only use for this
# subfunction when working with Perl under DOS.
#
-----------------------------------------------------------------
--

$old_dev = ioctl( STDIN, 0, 0) ;        # Get device data
$old_dev &= 0xFF ;                      # Clear top bits, which
we can't set.
$new_dev = $old_dev | 32 ;              # Set bit 5, for raw
mode.ioctl( STDIN, 1, $new_dev) ;               # Set the mode.

$c = '' ;                               # Create our input
buffer.print "press a character to continue ";
sysread( STDIN, $c, 1) ;                # Read just one byte
print " thanks...\n" ;

ioctl( STDIN, 1, $old_dev) ;            # reset the mode.

#
-----------------------------------------------------------------
--# Functions 02 to 05 would be much easier to try out if I had
a device
# driver that used them...
#
-----------------------------------------------------------------
--

#
-----------------------------------------------------------------
--# Is device ready for input?
#
# This is useful if you're using raw mode terminal i/o via
sysread(), # as Perl's EoF function doesn't work under these
conditions, and
# reading <STDIN> appears to crash Perl.
#
# FILEHANDLE is the filehandle we're interested in.
# FUNCTION is 6
# SCALAR is ignored, and should be zero.
# The return value is undefined for an error, 0-but-true for
device# not ready (or EoF on an input file), or 255 for input
ready.#
# This example sets raw i/o for STDIN, loops until you hit a key
# and then reads input using sysread().
#
-----------------------------------------------------------------
--

$old_dev = ioctl( STDIN, 0, 0) ;        # Get device data
$old_dev &= 0xFF ;                      # Clear top bits, which
we can't set.
$new_dev = $old_dev | 32 ;              # Set bit 5, for raw
mode.ioctl( STDIN, 1, $new_dev) ;               # Set the mode.

do
{       $value = ioctl( STDIN, 6, 0) ;  # Get status
       die "ioctl failed: $!\n"        # Handle error return
               unless $value ;
       print '.' ;                     # Prove we're looping
} until ($value > 0) ;                  # Exit when get some
input.

print "ready to read!\n" ;              # Prove we've noticed
$line = '' ;                            # Suppress a warning...
sysread( STDIN, $line, 1) ;             # Read the character
print $line ;                           # Print it out

ioctl( STDIN, 1, $old_dev) ;            # Reset to cooked mode.

#
-----------------------------------------------------------------
--# Is device ready for output?
#
# Function 07 isn't very useful unless we are driving printers
from Perl,
# as it always returns "ready for output" for disk files,
irrespective # of the actual conditions - even if the disk is
full, or there is
# no disk in the drive.  However, it may let us tell if a
printer is
# off-line. It isn't much use for screen output as that's always
ready.#
# FILEHANDLE is a handle on the printer we're interested in.
# FUNCTION is 7
# SCALAR is ignored, and should be zero.
# The return value is undefined for an error. The low byte of
the # return value is 0 for not ready, or 255 for ready. The
significance # of the high byte's value is unknown: it is always
1.#
# This example tests the name supplied on the command line. A
filename# is always ready; an off-line printer with a separate
buffer-box returns
# ready; a serial port with nothing plugged into it returns
not-ready.#
-----------------------------------------------------------------
--

open( PRINTER, ">$ARGV[0]") || die "Can't open file/device:
$!\n" ;
$value = ioctl( PRINTER, 7, 0) ;                # Do the ioctl
die "ioctl failed: $!\n" unless $value ;        # Handle the
error$value &= 0xFF ;                           # Select the AL
valueprint "File/device ",                              # Report
it.     ($value>0 ? "is" : "isn't"), " ready\n"


#
-----------------------------------------------------------------
--# Does drive use removable media?
#
# FILEHANDLE is a drive number obtained from @fdnum, set up as
above.# FUNCTION is 8
# SCALAR is ignored, and should be 0.
# The return value is undefined for an error, 0-but-true for
removable# media or 1 for fixed media.
#
# This example queries the drive whose number is in $drive
(0=default, # 1=A:, 2=B:, etc...)
#
-----------------------------------------------------------------
--

$value = ioctl( $fdnum[$drive], 8, 0) ;
die "ioctl failed: $!\n" unless $value;
print "Drive ", ($value ? "doesn't" : "does"), " use removable
media\n" ;

# Note the negative logic: DOS returns 1 for *fixed* media.

#
-----------------------------------------------------------------
--# Is drive remote? (plus other attributes for Local drives)
#
# FILEHANDLE is a drive number obtained from @fdnum, set up as
above.# FUNCTION is 9
# SCALAR is ignored, and should be 0
# The return value is undefined for an error or the drive
attributes # from the DX register.
#
# This example queries the drive whose number is in $drive
(0=default, # 1=A:, 2=B:, etc...)
#
-----------------------------------------------------------------
--

$value = ioctl( $fdnum[$drive], 9, 0) ;
die "ioctl failed: $!\n" unless $value;
printf( "Drive attributes are %x\n", $value) ;

#
-----------------------------------------------------------------
--# Is file or device remote?
# FILEHANDLE = filehandle
# FUNCTION = 0x0A
# SCALAR is ignored, and should be zero
#
# The return value is undefined for an error or the attributes
# from the DX register. Bit 15 -> remote.
#
-----------------------------------------------------------------
--

open( INPUT, $ARGV[0]) || die "Can't open $ARGV[0]: $!\n" ;
$value = ioctl( INPUT, hex( 'A'), 0) ;
die "ioctl failed: $!\n" unless $value;
printf( "Device attributes are %x\n", $value) ;

#
-----------------------------------------------------------------
--# Set sharing retry count
#
# This sets the number of retries on a sharing clash, and the
# interval between them. Note that there is no way to *read* the
# current values of these settings.
#
# FILEHANDLE = Not relevant, but should be a Perl filehandle to
keep# Perl from getting upset.  We'll use STDIN in this example.
# FUNCTION = Low nibble is 0xB, upper three nibbles are the wait
between# tries on file operations. This is done via a delay loop
and the time
# it takes depends on the computer's clock speed. The default
value is
# 1, and applications that change it should reset it before
exiting.# SCALAR = Number of retries to make for file operations
before giving
# up and returning an error.  The default is 3; again, programs
that# change it should restore the default value.
#
# The return value is undefined for an error or defined for
success.# <<<< Note that this function should always fail unless
SHARE.EXE or
# equivalent has been loaded. Currently, it doesn't.
#
-----------------------------------------------------------------
--$value = ioctl( STDIN, hex( "4B"), 6) ;
die "SHARE not available: $!\n" unless $value ;

#
-----------------------------------------------------------------
--# Get/Set device iteration count (number of retries before
assume busy)
#
# These are the simplest ioctl functions that use data from a
buffer # at DS:DX, or write into it ; this example was
constructed to find out
# how that was handled.
#
# FILEHANDLE = filehandle
# FUNCTION = 0x545C or 0x565C: Category 5 for parallel printer,
# subfunctions 45 or 65 of Ioctl function 0xC
# SCALAR is the buffer
#
# The return value is undefined for an error or defined for
success.#
-----------------------------------------------------------------
--

open( PRINT, ">prn") || die "Can't open printer: $!\n" ;

$set_itt = hex( "545C") ;
$get_itt = hex( "565C") ;

$in = pack( "s1", 60) ;

$value = ioctl( PRINT, $set_itt, $in) ;         # Set iteration
count to 60
die "Ioctl failed: $!\n" unless $value ;        # Handle errors

$dic = "\0" x 256 ;                             # Zero the
buffer for clarity
$dic = '' ;                                     # Count appended
to the string

$value = ioctl( PRINT, $get_itt, $dic) ;        # Read iteration
countdie "Ioctl failed: $!\n" unless $value ;   # Handle errors

$res = unpack( "s1", $dic) ;                    # Get the value
print "Iteration count is $res\n" ;             # Display it.

# Under DDS' Perl 3.041, this works. Under Eelco's current
4.036, it
# always returns zero.

<<<<< Functions to write up (partial) examples:

0C      Get/set code page and display mode options.
0D      Block Device params/format/read/write
0E      Get logical drive map
0F      Set logical drive map

#################################################################
#####
#       Bugs
#
#################################################################
####

<<<< In Eelco's current 4.036, Ioctl returns into a buffer don't
work.

The technique for getting drive numbers is a HORRIBLE kludge...
Obviously, it would be better if numeric FILEHANDLE parameters
could produce drive numbers, or if there were some other way
of doing it... As a basic patch, using INT21 Function 67H to
raise the number of program handles to 40 or thereabouts would
help.

Ioctl subfunctions 10h and 11h can't be accessed via the Perl
interface.These give an integrated way of finding out if devices
or drives
(respectively) support particular ioctl functions. They were
introducedin MS-DOS 5.0