X68000 Programming chapter 4.1, File operations.

Many times when we make a program we need to read or right files external to our program.
In the context of a video game it may be an image, a tile map or a PCM.
We may also nee to write in disk a log or save a game.

In X68000 we can utilise the <stdio.h> library a like we would in other operating system and that’s it, but the Human68k OS has its own set of functions to manage files.

Why should we use this functions when we have <stdio.h>? In nfg.forums there is this entry of Lyndux commenting about writing drivers for X68000. https://nfggames.com/forum2/index.php?topic=5417.0 and apparently in early stages of the operating system’s bootstrap Libc isn’t fully available yet, therefore in this scenario the most reliable is to use these functions instead of <stdio.h> ’s.

It’s also in order to preserve this knowledge that I make this post explaining it all.

In this article I’m going to talk about these functions:

int _dos_create (const char *file, int attr)

int _dos_newfile (const char *file, int attr)

int _dos_maketmp (const char *file, int attr)

int _dos_open (const char *file, int mode)

int _dos_read (int fno, char *buffer, int size)

int _dos_write (int fno, const char *buffer, int size)

long _dos_seek (int fno, int offset, int mode)

int _dos_close (int fno)

int _dos_delete (const char *file)

All of them belong to the <doslib.h> should we use Mariko or <dos.h> if Lydux toolchain.

Create a file:

int _dos_create (const char *file, int attr)

int _dos_newfile (const char *file, int attr)

int _dos_maketmp (const char *file, int attr)

In <stdio.h> we would use FILE *fopen(const char *filename, const char *mode) whether to open a file as well as to create it. Moreover fopen() will give us a pointer to the handler of the file we just opened or created which will be used with other functions.

In Human68k we have 3 different functions to create files and even directories; these are _dos_create(), _dos_newfile(), and _dos_maketmp().
Plus another function only to open an existing file with _dos_open().
These functions return an integer number that the operating system assign to the file and it comes to be like the file handler. If the file can’t be opened or created, the number returned will be negative and represents an error code.

The three functions to create a file have a string of characters with the file name as first param, then an integer number as a second param.

The integer we pass as a second param is a byte of flags in which each bit has a meaning.

According to this post https://www.target-earth.net/wiki/doku.php?id=blog:x68_devcode the bits signify this:

000: read and write, 1: R – read only
010: visible, 1: H – hidden
020: normal, 1: S – system file
030: not a volume, 1: V – Volume name
040: not a directory, 1: D – directory name
050: not an archive, 1: A – archive name.
06 – 15The other bits are ignored.

The letters R, H, S, V, D, A are attributes that can be seen with the command ATTRIB <file name> in the command line.

To ease things we can create the following macros:

#define F_ATTR_  0x00  //0b000000 //0 read and write mode and A attribute (default)
#define F_ATTR_R 0x01  //0b000001 //1 read only
#define F_ATTR_H 0x02  //0b000010 //2 Hidden
#define F_ATTR_S 0x04  //0b000100 //4 System file
#define F_ATTR_V 0x08  //0b001000 //8 Volume name
#define F_ATTR_D 0x10  //0b010000 //16 Directory name
#define F_ATTR_A 0x20  //0b100000 //32 Archive name

The three functions create files but with different shades:

int _dos_create (const char *file, int attr)If the file exists it can override it if bit 0 is 0
int _dos_newfile (const char *file, int attr)If the file exists it won’t override it.
int _dos_maketmp (const char *file, int attr)If the file exists it will override it.

Then creating a file looks like this.

//We create a file and open it so we get its number to manage it.
file_number = _dos_create(
    "file", //file name
    F_ATTR_       //read and write mode and A attribute
);

//if any error...
if(file_number < 0) {
    _dos_c_print("Can't create the file\r\n");
}

The function _dos_create() has the file name as its first param and byte of flags as the second param.

Opening a file: int _dos_open (const char *file, int mode)

The function _dos_open() also have the file name as its first param and an integer of flags as the second in which each bit has a meaning.

In X68000, as we saw earlier there are many functions which instead of having a parameter for each aspect of the configuration they use an integer of flags.

In order to ease the use of the function we can build a macro or a bit-field structure.

The meaning of each bit is as follows:

Bits 0 -1 opening mode00: write only. 01: read only. 11: read and write.
Bits 2, 3 y 4, permissions to share with other processes whilst open000: default mode. 001: no one else has access to it. 010: allow read only to other processes. 011: allow write only to other processes. 100: read and write to other processes.
Bit 70: as a normal file, 1: as a dictionary (whatever this is)

To better manage these flags we can declare these macros:

#define OPEN_ACCESS_DICTIONARY           0x80    //0b10000000 //1 not for users
#define OPEN_ACCESS_NORMAL               0x00    //0b00000000 //0

#define OPEN_SHARING_TOTAL               0x08    //0b00010000  //4 allow others to write and read
#define OPEN_SHARING_WRITE_ONLY          0x0C    //0b00001100  //3 allow others to write only
#define OPEN_SHARING_READ_ONLY           0x08    //0b00001000  //2 allow others to read only
#define OPEN_SHARING_RESTRICTED          0x04    //0b00000100  //1 don't allow others anything
#define OPEN_SHARING_COMPATIBILITY_MODE  0x00    //0b00000000  //0

#define OPEN_MODE_RW                     0x02    //0b00000010  //2 open for write and read
#define OPEN_MODE_W                      0x01    //0b00000001  //1 open only for writing
#define OPEN_MODE_R                      0x00    //0b00000000  //0 open only for reading

These macros have the bits calculated in hexadecimal already. It only leaves to use another macro to put them together.

#define OPENING_MODE(access, sharing, mode) (access | sharing | mode)

The code would be like this:

//now we open the same file and capture the handler number
file_number = _dos_open(
    "file",          //file name
    OPENING_MODE(
        OPEN_ACCESS_NORMAL,
        OPEN_SHARING_COMPATIBILITY_MODE,
        OPEN_MODE_R  //only read mode
    )
);

In this example we are opening the file “file” and with the macro OPENING_MODE and the three params we calculate the flags for the second params.

Reading from file: int _dos_read (int fno, char *buffer, int size)

To read from a file we use the function _dos_read() to which we pass the file number that we obtained with the functions _dos_create(), _dos_newfile(), _dos_maketmp() or _dos_open().
As a second param the pointer to a buffer and as a third param the number of bytes.
The function returns an integer number with the number of bytes that have been read. If the file is smaller than the number of bytes or we reached the end of the file, this number will be the bytes that it has been able to read.

The buffer has to be an array of char. Should we need to store data in a different format we have to do a casting.

//we read the file and capture the number of characters we manage to read
int16_t bytes_read = _dos_read(
    file_number,        //handler number
    (char *) buffer,    //destination
    sizeof buffer       //bytes to read
);

As it can be appreciated it’s very similar to the function fread() of <stdio.h>.

Writing in a file: int _dos_write (int fno, const char *buffer, int size)

To write in a file we use the function _dos_write() to which we pass the file number just like in _dos_read(), the buffer as a char array and the size in bytes.

//we write the message in the file
status = _dos_write (
    file_number,    //this is like the file handler in stdio but represented by an integer
    message,        //the message
    sizeof message  //the size of the message
);

If an error happened it will return a negative number indicating the type of error.

Moving around the file: long _dos_seek (int fno, int offset, int mode)

Similarly to <stdio.h>, Human68k provides us with a function to move the pointer of the file forwards, backwards or at any random place.
The function is _dos_seek() and accepts 3 parameters. The first param is the file number, the second is the offset and the third param is the point the offset is relative to.
This third param can be 0: beginning of the fila, 1: from the spot the pointer is currently in, 2: from the end of the file.

The function returns the byte number where the pointer is now on or a negative number it there was an error.

status = _dos_seek(
    file_handler,
    0, //offset
    2  //0 = beginning, 1 = on the spot, 2 = end
);

In this example we are moving the pointer to the end of the file to obtain the byte number at the end and so the size of the file.

Closing a file: int _dos_close (int fno)

Simply we close the file by passing the file number as a param.

//now we close the file
status = _dos_close(file_handler);

If no issues it will return 0, otherwise it will return a negative number indicating the error.

Deleting a file: int _dos_delete (const char *file)

To delete a file we use _dos_delete() with the file name as the only param.

//now we delete the file
status = _dos_delete("file");

We will get 0 if no issues or a negative number otherwise.

Errors:

As we’ve seen all the functions return either zero or a positive number indicating some detail of interest of no issues and a negative number if any error.
These negative numbers identify the error that happened and here is the list of errors:

-1 Executed invalid function code

-2 Specified file not found

-3 Specified directory not found

-4 Too many open files

-5 Cannot access directory or volume label

-6 Specified handle is not open

-7 Memory manager region was destroyed

-8 Not enough memory to execute

-9 Invalid memory manager pointer specified

-10 Illegal environment specified

-11 Abnormal executable file format

-12 Abnormal open access mode

-13 Error in selecting a filename

-14 Called with invalid parameter

-15 Error in selecting a drive

-16 Cannot remove current directory

-17 Cannot ioctrl device

-18 No more files found”; break;

-19 Cannot write to specified file

-20 Specified directory already registered

-21 Cannot delete because file exists

-22 Cannot name because file exists

-23 Cannot create file because disk is full

-24 Cannot create file because directory is full

-25 Cannot seek to specified location

-26 Specified supervisor mode with supervisor status on

-27 Thread with same name exists

-28 Interprocess communication buffer is write-protected

-29 Cannot start any more background processes

-32 Not enough lock regions

-33 Locked; cannot access

-34 Handler for specified drive is opened

-35 Symbolic link nest exceeded 16 steps (lndrv)

-80 File exists

In a next issue I will speak about functions to create directories, change attributes and make searches of files and directories.

Leave a comment