OpenCP Device-Development :)

Tmbinc

Introduction to FDOs, DLLs and the CP in general

In the meantime, OpenCP 2.5.0 (available on http://www.cubic.org/player) has already been released. For those who don't know it: OpenCP is the sequel to the Cubic Player, one of the best (if not THE best :) module-players.

Of course, no player can play all formats without errors, support all soundcards or open all archive formats. While you would have to wait for a new version of "normal" players, you can do something against it for Cubic Player by yourself.

From the technical point of view this is possible since CP 1.666. Here, the so-called .FDOs were introduced, "flatmode dynamic link objects". All in all these are nothing different than DLLs which everyone knows from Windows, OS/2 or also Unix. FDOs contain e.g. drivers for soundcards. If a new soundcard was to be supported, not a new Cubic Player would have to be released but only a new FDO.

With an entry in CP.INI, you can embedd such a driver.

In Cubic Player 2, these FDOs were replaced by DLLs, i.e in general only a different format (however, I am not sure whether FDO also supported imports.)

The format FDO had been invented by pascal, which means that it was not supported by any linker. A different thing is the case with the DLLs of CP2. They are simply normal LE-DLLs. Everybody who has done a little more with Watcom C and DOS4G or PMode/W should know the LE-format: It is used there by default.

In general, there is no great difference between LE-DLLs and LE-EXEs. LE-EXEs, however, usually contain no imports or exports (the right professional-version of DOS4GW, DOS4G, supports DLLs).

To reach that CP also works with the normal DOS4GW or even PMode/W, CP contains a DLL-loader which loads these DLLs and links them to the main program. The main program merely consists only of this DLL-loader (later more about this).

Everyone who has Watcom C can create LE-DLLs. Of course it's also possible with other linkers and compilers, but you ought to use Watcom.

How to create DLLs

If you have already created DLLs once or more, you can probably ignore this chapter.

Otherwise, it isn't so hard either. Everyone of you has probably made a program with more than one object file. The various object files were linked to a single large EXE in the end.

With DLLs, you usually don't have to change a single line of code.

You link one or more object files to a DLL. If other DLLs access to symbols (in this case, symbols are variables and functions) in a DLL, these symbols have to be exported from the DLL which contains them and imported in the DLL which uses these symbols.

The linker does that by itself. You just have to tell it where what symbols can be found.

For this purpose, import libraries exist. They are similar to normal libraries apart from the fact that they don't contain normal code but just links to the DLL. You handle these import libraries like normal libraries (and object files), you just link them to the rest. When the linker needs some function which is described in the library it creates an import (i.e a relocation/fixup entry; the target is the corresponding DLL and the corresponding symbol name).

You have to export by EXPs, which are files that contain a list with the to-be-extracted symbols.

On linking you pass them to the linker which then creates exports. (An export is just an entry in the entry-table but with a name.)

EXPs just look like the following:

 'printf_'
 '_i'

...to export the function printf and the variable i. You create import libraries by

 WLIB x.LIB +x.DLL

presumed that x.DLL contains exports (logical, huh? :)

The main EXE is actually just such a DLL apart from the fact that it doesn't contain any imports but an entry-point to main_.

The construction of OCP

All DLLs of OCP are in CP.PAK, which is a Quake-compatible .PAK file. You can unpack it with normal Quake tools.

I roughly describe the most important DLLs which are in this .PAK file:

 File name       Content
 ARC*.DLL        archive-reader (e.g. ARCZIP.DLL for .ZIP-files)
 CPIFACE.DLL     the interface (the screens with trackview and so on)
 DEVI.DLL        auxilary  routine  for  the  devices  system  that  reads from
                 CP.INI(*) what devices are to be loaded and loads them
 DEVP*.DLL       player-devices, play samples
 DEVS*.DLL       sampler-devices,  are  more or less  able  to sample (e.g. for
                 displaying the FFTs of Audio CDs)
 DEVW*.DLL       wavetable-devices,  play more samples at the same time, either
                 directly  on the hardware (GUS, SB32...)  or mix them and play
                 them on DEVPs (DEVWMIX, DEVWMIXQ)
 FSTYPES.DLL     module-detection,  song-infos  for  various module-formats are
                 evaluated here
 HARDWARE.DLL    routines  for  bending and using IRQs,  the  timer and DMAs so
                 that not every device driver needs its own functions
 LOAD*.DLL       module-loaders, load various formats
 PFILESEL.DLL    the file selector
 PLAY*.DLL       plays modules on various devices

(*) Since version 2.0m2 many things are not directly retrieved from CP.INI but from an exported _dllinfo. Read more about this later.

A simplified overview about OCP:

CP.EXE loads important DLLs, starts DEVI to get the desired devices, and starts the file selector. This program part asks the user which module he wants, seeks the corresponding LOADer, loads the module, seeks the corresponding PLAYer, and passes the module to it. The player seeks the corresponding devices and passes the data that should be played to them.

At the same time the player passes data to the interface, which was initialized some time before. The interface displays these data and evaluates inputs from the user.

The documentation of OCP contains a nice block picture, which demonstrates it quite well.

Our first Cubic-DLL

To create a DLL, you first have to create an environment where you can do this best.

The easiest method is to install the source of the CP and put one's DLL into the makefile.

However, since the directory gets messy soon, I recommend developing one's DLLs elsewhere.

At first you should get all .LIBs and .Hs of the CP by compiling the CP once. (Soon there is said to come an archive with all .LIBs and .Hs for those people who just want to develop something new. :)

A (in my opinion) beautiful environment is attached to Hugi. :)

Let's start it all over from the beginning step by step.

As an example, we want to write an ACE-reader which displays the descriptions before the final decompressing.

First depack the environment, set the paths correctly and copy the TEMPLATE from the INDEV directory to INDEV\ARCACE.

There you have to change the makefile, namely in this way:

 dest = arcACE.dll

...

 arcace_desc = 'OpenCP .ACE Archive Reader (c) 1998 Felix Domke'
 arcace_objs = arcace.obj
 arcace_libs = cp.lib pfilesel.lib
 arcace_ver = 0.0

 arcace.dll: $(dlldeps) arcace.exp $(arcace_objs) $(libdir)$(arcace_libs)
   $(makedll)
   copy arcace.dll \opencp\bin

You have to enter all destinations for dest, i.e all DLLs which shall be created in the end.

arcace_desc is the description which will be printed e.g. beside ' in CP.

arcace_objs are the object-files which shall be linked to the DLL.

Since there is an implicit-rule which says that WPP386 gets executed for CPPs for which OBJs with the same name are demanded, there also has to be an arcace.cpp with our code. :)

arcace_libs are the (import-)libraries from which the imports shall be searched. Standard functions like sprintf_ etc. are exported in CP.LIB. PFILESEL.LIB contains some functions we will need later.

arcace_ver is the version which gets displayed in the linkview (') of CP. You compute it in a rather strange way. How exactly, I myself haven't understood, but usually you should convert the version from hex to decimal (e.g. 0x024000 for 2.5.0) and then divide by 100... But someone must have erred.

The copy at the end copies the DLL right into the CP directory. :)

Now we FINALLY can get to coding. :)

In general in an archive-read, two functions are passed, one to read the file names from the archive and "tell" them to the file selector via callback and another to call the unpacker to unpack a file from the archive.

To be concrete, it looks like the following:

An adbregstruct gets exported which looks like this:

 struct adbregstruct
 {
   const char *ext;
   int (*Scan)(const char *path);
   int (*Call)(int act, const char *apath, const char *file, const char *dpath);
   adbregstruct *next;
 };

ext is the extension, in our case ".ACE".

Scan is a function which gets called with the archive as parameter and tells it to the file selector by calling adbAdd().

Call will get executed then to unpack a file from the archive.

We leave out next because it will be set at runtime anyway.

Now let's export such a struct:

 extern "C"
 {
  adbregstruct adbACEReg = {".ACE", adbACEScan, adbACECall};
  char *dllinfo = "arcs _adbACEReg";
 };

dllinfo makes an entry in CP.INI superfluous. It provides that CP takes notice of our archive-reader.

However, something like link=... arcace.dll has to be in CP.INI.

Our adbACEScan() looks like the following:

(The function has been STRONGLY simplified and can't be run in THIS form, but all things concerning OpenCP archives are in it. Only the ACE-specifical things, most of all the thing with the SOLID-archives, are missing.)

 static int adbACEScan(const char *path)
 /*
   "path" is the filename of the archive.
   If everything works, 1 will be returned, otherwise 0.
 */
 {
  [...]
  sbinfile archive;
 /*
   "binfile" is the Library which is used in CP for (almost) all file accesses.
   I think it is EXTREMELY practical. It is actually quite self-explanatory.
   "sbinfile" is a normal file on disk.
 */
  if(!archive.open(path, sbinfile::openro))
   return(1);
 /*
   When we can't open the archive, we can exit at once :)
 */

  unsigned short arcref;

  char arcname[12];
  char ext[_MAX_EXT];
  char name[_MAX_FNAME];
  _splitpath(path, 0, 0, name, ext);
  fsConvFileName12(arcname, name, ext);
  arcentry a;
  memcpy(a.name, arcname, 12);
  a.size=archive.length();
  a.flags=ADB_ARC;
  if (!adbAdd(a))
  {
   archive.close();
   return 0;
  }
 /*
   Here, the archive itself gets added to the file listing, namely by
   adbAdd(arcentry &).
   The struct which adbAdd expects looks like:

   struct arcentry
   {
    unsigned short flags;
    unsigned short parent;
    char name[12];
    unsigned long size;
   };

   "flags" is e.g. ADB_ARC for archives.
   "parent" is not so important at the moment.
   "name" is the file name in 8.3-format (fsConvFileName12 converts it
          pretty nicely :) (e.g. from "x.y" to "X       .Y  ")
   "size" is simply the length which will be displayed.
 */
   arcref=adbFind(arcname);
 /*
   Aferwards we keep the "ref" in our mind so that we can enter it as "parent"
   afterwards. parent is the source-archive so that the files there will be
   unpacked from the right archive afterwards...
   (The file selector creates a list of all files in the directory and the
   files in the archives. For the last thing, it calls the archive-reader. If
   the user presses with "Enter" on a file afterwards, the file selector has to
   know what ARCer it has to call and most of all from which archive the files
   come. Therefore the parent has to be set. Easy, huh?)
 */
  [...]

 /*
   Now let's deal with the ACE itself. The ACE format is very similar to the
   RAR one, a detailed format description is also included in this Hugi issue...
   In any case the function "ReadNextHeader" reads the header on the current
   position and skips additional bytes if they appear (i.e with files the
   packed data). In this way header comes after header, at least it appears.
   (In reality ReadNextHeader SEEKs at the beginning, not at the end, but
   that's not important now, don't get confused by that :)
 */

  arcentry a;
  while(ReadNextHeader(archive))
  {
 /*
    Here, the header gets read.
 */
   char ext[_MAX_EXT];
   char name[_MAX_FNAME];
   [...]
   if(aheader->type==1)
   {
 /*
   ...if the type is 1 (file), the file name will be read...
 */
    [...]
    char filename[_MAX_PATH];
    memcpy(filename, afheader->filename, afheader->filenamesize);
    filename[afheader->filenamesize]=0;
    strupr(filename);
    _splitpath(filename, 0, 0, name, ext);
 /*
    ...splitted into "name" and "ext"...
 */
    if(fsIsModule(ext))
 /*
    ...and if "ext" is a module-extension (see CP.INI), then...
 */
    {
     a.size=afheader->unpsize;
     a.parent=arcref;
     a.flags=0;
     fsConvFileName12(a.name, name, ext);
 /*
    ... this file will be added to the file list by adbAdd.
 */
     if(!adbAdd(a))
     {
      archive.close();
 /*
    If the whole thing doesn't work, well, then it doesn't work.
 */
      [...]
      return 0;
     } else
     {
 /*
    Otherwise, if wished,
 */
      if(fsScanInArc&&[...])
      {
 /*
    ...some blocks get be depacked (by the UNACE-routines in UAC_DCPR.*)...
 */
       dcpr_init_file();
       int rd=dcpr_adds_blk(buf_wr, size_wrb);
       unsigned short fileref;
       fileref=mdbGetModuleReference(a.name, a.size);
 /*
    ...a reference for the just ADDed file will be retrieved...
 */
       if((fileref!=0xFFFF)&&(!mdbInfoRead(fileref)))
       {
        moduleinfostruct ms;
        if(mdbGetModuleInfo(ms, fileref))
        {
         mdbReadMemInfo(ms, (unsigned char*)buf_wr, rd);
 /*
   ...and then we e.g. look for the song name etc. by "mdbReadMemInfo". Just
   for explanation, again:

   "buf_wr" contains about the first unpacked 2-4kb of the actual file. Now
   "mdbReadMemInfo" tries to get to the infos in it with various,
   format-specific routines.
 */
         mdbWriteModuleInfo(fileref, ms);
 /*
   These will also be set then.

   Another thing about the "mdbInfoRead" and "mdbGetModuleInfo":
   At first you have to read what is already known about the file, then
   actualize it and write it back.
 */
        }
       }
      }
     }
    }
    [...]
   }
  }
  [...]
  archive.close();
  return(1);
 }

Well, that was not that difficult, was it? :)

Now let's come to adbACECall. This function is quite easy. Depending on act, it has to pack, unpack, move etc. the file file from the archive apath to dpath. To make it easier, only one act=="adbCallGet" is supported, i.e unpacking.

 static int adbACECall(int act,
                       const char *apath,
                       const char *file,
                       const char *dpath)
 {
  switch (act)
  {
   case adbCallGet:
   {
    return !adbCallArc(cfGetProfileString("arcACE", "get", "ace e %a %n %d"),
           apath, file, dpath);
 /*
    "adbCallArc" is a nice function: it replaces something like %a, %n and %d
    with the passed arguments. The first argument has to be inserted for %a,
    the second for %n and the third for %d.
    Beforehand, "cfGetProfileString" gets from CP.INI whether the user wants to
    use another packer (XACE etc.) and that in the section "arcACE" and the key
    "get=".
    If the key hasn't been found, "ace e %a %n %d" will be taken by default,
    which, by the way, should be correct almost every time. :)
 */
   }
   case adbCallPut:
    // not implemented
    break;
   case adbCallDelete:
    // not implemented
    break;
   case adbCallMoveTo:
    // not implemented
    break;
   case adbCallMoveFrom:
    // not implemented
    break;
   }
   return 0;
 }

Now a simple wmake compiles the whole thing presumed that a working makefile is available...

That's it for the beginning. If there is enough interest, I will wrote a second part for Hugi #13. It will be about a DLL for the interface.

Comments, questions, flames, sources, money, well, actually everything to:

tmbinc@gmx.net

I think that greetings in mags are stupid. Therefore here are none.