# DOSIOCTL.PL: a Perl file of examples for ioctl() under MS-DOS,
# the previous documentation not being of the very finest.
#
# Author: John Dallman (jgd@cix.compulink.co.uk)
# Thanks to Dan Carson (dbc@tc.fluke.com) 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 . (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 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