Trying to do CP/M file I/O with the classic library...

alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Trying to do CP/M file I/O with the classic library...

Post by alank2 »

I put together some test code to test the functions, but it goes crazy when I try to do the fopen command.

Code: Select all

#include "stdio.h"
#include "conio.h"

unsigned char c[2048];

int main()
{
  FILE *file;
  int i1;

  for(;;)
  switch(getch())
    {
      case 'o' : printf("open - ");
                 file=fopen("test.dat","rb");
                 if (file==NULL)
                   printf("fail\n");
                 else printf("ok\n");
                 break;

      case 'O' : printf("create - ");
                 file=fopen("test.dat","wb");
                 if (file==NULL)
                   printf("fail\n");
                 else printf("ok\n");
                 break;

      case 'c' : printf("close\n");
                 fclose(file);
                 break;

      case 'r' : printf("read - ");
                 i1=fread(c,2048,1,file);
                 printf("result %d\n",i1);
                 break;

      case 'w' : printf("write - ");
                 i1=fwrite(c,2048,1,file);
                 printf("result %d\n",i1);
                 break;

      case '0' : printf("c=0\n");
                 memset(c,0,2048);
                 break;

      case '1' : printf("c=0\n");
                 memset(c,1,2048);
                 break;

    }

  return 0;
}
Result:

Code: Select all

open - test.dat rb fail
 ok
 create -  wb close
 read -  result %d
 write -  c=0
 w r   ♦ ☻ ♠ ☺ ♣ ☺  ♦ ♠ ☻ ♠ ☺ ♣ ♥      ‼     ?     ?
                   ic   TEST    DAT                            ☺
User avatar
dom
Well known member
Posts: 2076
Joined: Sun Jul 15, 2007 10:01 pm

Post by dom »

Looking at the code, there's a call to bdos in the #asm section of libsrc/fcntl/cpm/parsefcb.c:128 - I think this will end up calling the C wrapper function rather than the actual bdos function.

As a result the bdos function will be called with total garbage, and I'm not sure how a real platform/emulator would handle this - zxcc simply bombs out.

I'm away for a few days so I'm not going to have time to investigate properly, but if you replace "call bdos" with "call 5" and then rebuild the cpm library you might get a bit further:

cd libsrc
rm cpm_clib.lib
make cpm_clib.lib
cp cpm_clib.lib ../lib/clibs

If you're stuck I'll be able to look into it next week.
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

I'm on windows so I don't know how to build the classic library...
User avatar
dom
Well known member
Posts: 2076
Joined: Sun Jul 15, 2007 10:01 pm

Post by dom »

Ok, I've fixed it up so your example works for both read and write. There's a bit more work that I need to do to be entirely happy, but that will have to wait until I return.
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

Thank you; let me know where/when there is a new build I can download or the cpm_clib.lib file.
User avatar
dom
Well known member
Posts: 2076
Joined: Sun Jul 15, 2007 10:01 pm

Post by dom »

The next nightly should have it in
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

Super - I'll look for it!!! Thank you!
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

That fixed the fopen - I can now read and write files.

I read in CP/M that you can open a file for read or open it for read/write. If I open it for "rb" it is read and "wb" is write, but how can I open it for read/write? I usually use "r+b", but it fails. (I realize the b probably doesn't do anything). I'm having trouble with fseek as well, it doesn't seem to work.
User avatar
dom
Well known member
Posts: 2076
Joined: Sun Jul 15, 2007 10:01 pm

Post by dom »

It looks like I never supported O_RDWR for fopen, depending on the port it will work for open() though. So I'll need to update it for that.

I'll look at fseek as well - I think I might have a logic check the wrong way round - I think it will vanish into space when you call fseek and for an added bonus it's not sdcc safe either.

Hopefully I'll be able to sort them out in the next few days.
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

Thanks!!
User avatar
dom
Well known member
Posts: 2076
Joined: Sun Jul 15, 2007 10:01 pm

Post by dom »

I've added support of r+ and w+ within stdio, and they work with +test target, and something happens with CP/M.

I'm not sure whether fseek is working - we set the ranrec entry within the fcb (see https://en.wikipedia.org/wiki/File_Control_Block), but John Elliot has this for seeking using the fcb: https://www.seasip.info/Cpm/fcb.html and of course I'm using zxcc as my emulator!
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

If I switch to using the new compiler (sdcc), I will lose functions like fopen/fread/fwrite, is that correct? Can I just write my own disk functions that all bdos in that case? Do you have any examples of passing C parameters to assembly and back that I can look at?
User avatar
dom
Well known member
Posts: 2076
Joined: Sun Jul 15, 2007 10:01 pm

Post by dom »

It depends. You can stay with classic library and keep file io (-compiler=sdcc) or you can switch over to newlib (-clib=sdcc_ix or -clib=sdcc_iy) and lose fileio.

For cpm that is really the main difference - most of the standard non stdio code is now shared between the two libraries.
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

Super - do you have an example of a C function that passes/receives arguments to/from some assembly code?
User avatar
dom
Well known member
Posts: 2076
Joined: Sun Jul 15, 2007 10:01 pm

Post by dom »

There's a load of examples in libsrc :)

There's some documentation here: https://www.z88dk.org/wiki/doku.php?id= ... y_language and https://www.z88dk.org/wiki/doku.php?id=usage:stackframe

The general pattern is to create two files: one which contains the C interface and the other an assembler implementation. The C interface is responsible for gather than stack arguments into registers and the assembler for actually doing it.

Probably the easiest way to understand this is to take a look at libsrc/_DEVELOPMENT/string/z80/*.asm and libsrc/_DEVELOPMENT/string/c/sccz80/*.asm - that's the implementation of string.h that's accessible from both C and assembler.

To complicate things we do have a few calling conventions that change the C binding:

* __smallc - this passes function arguments from left to right. It's the natural calling convention of sccz80
* __stdc - this passes function arguments from right to left - it's the natural calling convention of sdcc

These days, both compilers support both argument passing conventions, for the purposes of compatibility, the classic library is __smallc. The two compilers use differently named entry points into library code. For the function strcpy(), sccz80 uses strcpy and sdcc _strcpy. Most of the time both labels are at the same address, the only difference in classic is for vaarg functions.

* __z88dk_fastcall - if you have only a single argument, then it can be passed in (de)hl without usage of the stack
* __z88dk_callee - following every call to a function, the caller has to clear up the stack, this spec puts the onus on the callee to adjust the stack

There's some more which are only useful in certain situations:

* __z88dk_saveframe - This will save ix on entry to a sccz80 compiled function. This is necessary if the function uses any long values and is expected to be called by sdcc
* __z88dk_sdccdecl - This flag forces __stdc and makes the sccz80 calling convention match the sdcc calling convention for chars and only take up a single byte on the stack. You can use __z88dk_sdccdecl __smallc to force sdcc char convention and l->r argument passing. Generally in classic, chars are promoted to ints on function signatures to avoid this problem
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

Thanks - that gives me something to start with!!!
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

I tried the example from:

https://www.z88dk.org/wiki/doku.php?id=usage:stackframe

The new compiler doesn't like the #asm

I looked at some of the examples from: libsrc/_DEVELOPMENT/string/z80/*.asm

But I'm still lost...

Can a C only solution be done somehow with some inline assembly? I just need one example BDOS call!
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

It also compiles to main_CODE.bin and not main.com...
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

Using : zcc +cpm -clib=sdcc_iy main.c -omain.com
alvin
Well known member
Posts: 1872
Joined: Mon Jul 16, 2007 7:39 pm

Post by alvin »

alank2 wrote:https://www.z88dk.org/wiki/doku.php?id=usage:stackframe

The new compiler doesn't like the #asm
That documentation is a little bit old but it will work (the scoping directives XLIB,XDEF,XREF are deprecated in favour of PUBLIC,EXTERN,GLOBAL). As mentioned, the preference now is to put the asm implementation into a separate asm file and the C interface to the asm implementation, the code that gathers params from stack, in its own asm file as well.

You should replace "#asm" and "#endasm" with "__asm" and "__endasm;" (note the semicolon) as the latter is acceptable to both compilers.

There is a little more on this topic here:
https://www.z88dk.org/wiki/doku.php?id= ... y_language
I looked at some of the examples from: libsrc/_DEVELOPMENT/string/z80/*.asm
But I'm still lost...
I'll give one example here using strcat:

The asm implementation for strcat was written first and called "asm_strcat.asm". Convention has "asm_" at the front of the name.
https://github.com/z88dk/z88dk/blob/mas ... strcat.asm

Because there are two arguments the best function call linkage is callee where the compiler/caller pushes the arguments onto the stack and the asm implementation pops them off so that the caller doesn't have to fix the stack after the call.

The header file reflects the linkage (taken from the classic library):
https://github.com/z88dk/z88dk/blob/mas ... e/string.h

Code: Select all

extern char __LIB__ *strcat(char *dst,const char *src) __smallc;
extern char __LIB__ *strcat_callee(char *dst,const char *src) __smallc __z88dk_callee;
#define strcat(a,b) strcat_callee(a,b)
There is a bit of magic here but a typical call will be to strcat_callee. There is a second c interface to a related function strcat that uses standard c linkage instead of callee. This is because the compilers must use standard linkage for function pointers. So function pointers will be assigned strcat rather than strcat_callee and this decision is made by the pre-processor.

Then the c interface for strcat_callee using callee linkage is:
https://github.com/z88dk/z88dk/blob/mas ... callee.asm

and the c interface for strcat using standard linkage is:
https://github.com/z88dk/z88dk/blob/mas ... strcat.asm

We're trying to get maximum performance and that's why it's a little more complicated than a straightforward implementation.
Can a C only solution be done somehow with some inline assembly? I just need one example BDOS call!
Yes you can, from the example you quoted above:

Code: Select all

 int myfunc(char b, unsigned char *p) __smallc __naked
 {
    __asm
    
    ld hl,2
    add hl,sp              ; skip over return address on stack
    ld e,(hl)
    inc hl
    ld d,(hl)              ; de = p
    inc hl
    ld a,(hl)              ; a = b, "char b" occupies 16 bits on stack
                           ;  but only the LSB is relevant
    ...
    
    ld hl,1                ; hl is the return parameter
   ret
    
    __endasm
 }
The C wrapper is going to tell the C compiler how to call. The "__smallc" will ensure the compilers push parameters in left to right order. zsdcc will do right to left otherwise. The "__naked" ensures zsdcc will not put function prologue and epilogue around your asm code (and now a "ret" us required at the end).

Another point to consider: zsdcc pushes char types as one byte on the stack but sccz80 pushes two bytes for char. So either add some conditional assembly to distinguish the two ("IF __SDCC ... ELSE ... ENDIF") or avoid char parameters.

Any return value goes in a subset of DEHL. sccz80 expects 16-bits for char but zsdcc only 8. Float and 64-bit longlong (zsdcc only) are returned differently.

sccz80 allows any registers to be modified by asm but zsdcc requires ix to be preserved. If you have a call to bios in there you must save ix around the call for zsdcc.
alvin
Well known member
Posts: 1872
Joined: Mon Jul 16, 2007 7:39 pm

Post by alvin »

And of course I forgot the semicolon on "__endasm;" :P
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

Thanks everyone, I did add the semicolon!

I was going to ask why the ret is necessary, but then I saw the __naked

I had to also add
b:
p:

to get by the compiler warning about them not being used.

Is there a macro to make a pointer out of an address? to turn something like 0xA000 into an unsigned char * pointer?

It let me supply 0xffff in the function for p, but I wonder if there is a macro to do it the right way?
alvin
Well known member
Posts: 1872
Joined: Mon Jul 16, 2007 7:39 pm

Post by alvin »

alank2 wrote:Using : zcc +cpm -clib=sdcc_iy main.c -omain.com
It also compiles to main_CODE.bin and not main.com...
The toolchain always outputs binaries. It's the following appmake step that takes the binaries and the (hidden) map file to create another suitable format. You either have to invoke appmake on its own after a compile or you can ask zcc to do the default thing by adding "-create-app" to the compile line. If you add that you will get a .com for cpm compiles.

The other thing is that compile string uses the new c library and a better one that has optimization on would be:
zcc +cpm -clib=sdcc_iy -SO3 --max-allocs-per-node200000 main.c -o main -create-app
(and optionally --opt-code-size)
sccz80 compile:
zcc +cpm -clib=new main.c -o main -create-app

The newlib does not have disk io integrated into its stdio (maybe that's why you're calling bdos directly?) but the classic library does have it. A cpm compile for classic is:
zcc +cpm -compiler=sdcc -SO3 --max-allocs-per-node200000 --reserve-regs-iy main.c -o main.bin -create-app
sccz80 compile:
zcc +cpm main.c -o main.bin -create-app

Hopefully I haven't messed up the classic compiles there; I can't check on this machine.
alvin
Well known member
Posts: 1872
Joined: Mon Jul 16, 2007 7:39 pm

Post by alvin »

alank2 wrote:I had to also add
b:
p:

to get by the compiler warning about them not being used.
Yeah zsdcc might do that.
Is there a macro to make a pointer out of an address? to turn something like 0xA000 into an unsigned char * pointer?
Just a regular C cast will work, nothing fancy required:

(unsigned char *)0xa000
It let me supply 0xffff in the function for p, but I wonder if there is a macro to do it the right way?
I think C allows implied casting to a void* without warnings.
alank2
Member
Posts: 116
Joined: Wed Mar 01, 2017 7:24 pm

Post by alank2 »

I added -create-app and that does indeed make main.com

I've come up with a working thing for getch:

Code: Select all

int getch() __smallc __naked
 {
   __asm

   push af
   push bc
   push de

   ld c,6
   ld e,0xfd
   call 5

   ld h,0
   ld l,a

   pop de
   pop bc
   pop af


   ret
   __endasm;
 }
Do I have to push/pop registers that change? besides HL that are being returned?
If I make it return an unsigned char, can I just set L?
Other improvements? (it is probably obvious I am not a Z80 assembly programmer...)
Post Reply