My suggestion is as follows:

The Job Properties structure has a common header and the growable
part of the printer driver specific information.

typedef struct _DRIVDATA       /* driv */
{
   LONG    cb;
   LONG    lVersion;
   CHAR    szDeviceName[32];
   CHAR    abGeneralData[1];
} DRIVDATA;
typedef DRIVDATA *PDRIVDATA;

My solution would be to add another cb2 element at a fixed position that
is the size of the device specific job properties (these job properties
are in an unreadable format (binary data) to the OMNI driver much like
the OMNI driver's job properties are unreadable to the application).

Since the OMNI driver has existing job properties at abGeneralData
you cannot place it after szDeviceName.

*** Remember we must always be backwardly compatible!

So, another "fixed" location is the last double word (DWORD) in the job
properties.  If the omni driver adds more fields in its job properties
and moves the device's job properties down in memory and increases
the cb size accordingly, then you can get to the new cb2 element since
it is always the last DWORD.

    Ŀ
     cb             .Ŀ// This is the size of the entire block of memory
    Ĵ  
     lVersion         
    Ĵ  
     szDeviceName     
    Ĵ  
     OMNI driver's    
     job properties   
     that can be      
     growable |       
              |       
              v       
                      
 >Ĵ  
             ^       
             |       
    growable |       
    that can be      
    job properties   
    Device specific  
   Ĵ  
   ulong device ID   // This is located at cb - 3 * sizeof (ULONG)
   Ĵ  
   ulong SIGNATURE   // This is located at cb - 2 * sizeof (ULONG)
   Ĵ  
 . cb2              // This is located at cb - sizeof (ULONG)
    <

Way # 1)
--------

   The only problem here is that the new device specific code will need
   to read the data "backwards."  By "backwards," I mean that each
   individual element is in the normal direction but the next element
   is lower in memory.  For example

     Ŀ
     ...                      
     Ĵ
     third element (a boolean)
     Ĵ
     second element (a string)
     Ĵ
     first element (a ulong)  
     Ĵ
     ulong device ID          
     Ĵ
     ulong SIGNATURE          
     Ĵ
     cb2                      
     

   Remember that we must always be backwardly compatible.  When new elements
   are added, they are added at the ... location and if an old OMNI driver
   looks at it, then the device only reads the elements that it knows.
   If a new OMNI driver looks at it, it will read the new elements.

   So, it will require some awkward code, but it will be backwardly compatible.

   Now, the other issue is that whenever the OMNI driver reads *external*
   job properties (either from the DRIVDATA or from the ini files), it
   must translate them into the new level (for use *internally*).  For example:

      version [New level + 1] of the OMNI driver reads in version [New level]

      Ŀ
       cb             
      Ĵ
       lVersion       
      Ĵ
       szDeviceName   
      Ĵ
       OMNI driver's  
       job properties 
      Ĵ
       job properties 
       Device specific
      Ĵ
      ulong device ID 
      Ĵ
      ulong SIGNATURE 
      Ĵ
       cb2            
      

      It then splits the job properties in half

      Ŀ
       cb             
      Ĵ
       lVersion         Top Half
      Ĵ
       szDeviceName   
      Ĵ
       OMNI driver's  
       job properties 
      

      Ŀ
       job properties 
       Device specific  Bottom Half
      Ĵ
      ulong device ID 
      Ĵ
      ulong SIGNATURE 
      Ĵ
       cb2            
      

      Then, move the bottom half down, and initialize the new elements
      to 0 or what ever the defaults are.

      Ŀ
       cb             
      Ĵ
       lVersion       
      Ĵ
       szDeviceName   
      Ĵ
       OMNI driver's  
       job properties 
       new element 1  
       new element 2  
      Ĵ
       job properties 
       Device specific
      Ĵ
      ulong device ID 
      Ĵ
      ulong SIGNATURE 
      Ĵ
       cb2            
      

      The same strategy would hold true for adding things to the
      device specific dialog.

      On device initialization, a structure of only pointers
      would probably be best to access the device job property
      data.  For example (using the previous example)

         struct _EpsonJobProperties {
            PULONG   pul;    // points to proper place
            PSZ      psz;    //  in the job properties
            PBOOL    pf;     //  data
         } ejp;

         PBYTE  pbStart;

         pbStart = (PBYTE)pDrivData + pDrivData->cb - sizeof (ULONG);

         // Now you are looking at the cb2 element;

         pbStart -= sizeof (ULONG);  // Skip to ulong

         ejp.pul = (PULONG)pbStart;

         pbStart -= STRING_SIZE;     // Skip to string

         ejp.psz = (PSZ)pbStart;

         pbStart -= sizeof (BOOL);   // Skip to boolean

         ejp.pf  = (PBOOL)pbStart;


Way # 2)
--------

   With this way, the device specific job properties are in the "normal"
   order.

     Ŀ
     first element (a ulong)  
     Ĵ
     second element (a string)
     Ĵ
     third element (a boolean)
     Ĵ
     ...                      
     Ĵ
     ulong device ID          
     Ĵ
     ulong SIGNATURE          
     Ĵ
     cb2                      
     

   Remember that we must always be backwardly compatible.  When new elements
   are added, they are added at the ... location and if an old OMNI driver
   looks at it, then the device only reads the elements that it knows.
   If a new OMNI driver looks at it, it will read the new elements.

   Now, the other issue is that whenever the OMNI driver reads *external*
   job properties (either from the DRIVDATA or from the ini files), it
   must translate them into the new level (for use *internally*).  For example:

      version [New level + 1] of the OMNI driver reads in version [New level]

      Ŀ
       cb             
      Ĵ
       lVersion       
      Ĵ
       szDeviceName   
      Ĵ
       OMNI driver's  
       job properties 
      Ĵ
       job properties 
       Device specific
      Ĵ
      ulong device ID 
      Ĵ
      ulong SIGNATURE 
      Ĵ
       cb2            
      

      It then splits the job properties in parts

      Ŀ
       cb             
      Ĵ
       lVersion          Top Section
      Ĵ
       szDeviceName   
      Ĵ
       OMNI driver's  
       job properties 
      

      Ŀ
       job properties 
       Device specific   Middle Section
      

      Ŀ
      ulong device ID 
      Ĵ
      ulong SIGNATURE 
      Ĵ   Bottom Section
       cb2            
      

      Then, move the middle section down, and initialize the new elements
      to 0 or what ever the defaults are, and place a new bottom section
      at the end.

      Ŀ
       cb             
      Ĵ
       lVersion       
      Ĵ
       szDeviceName   
      Ĵ
       OMNI driver's  
       job properties 
       new element 1  
       new element 2  
      Ĵ
       job properties 
       Device specific
       new element 1  
      Ĵ
      ulong device ID 
      Ĵ
      ulong SIGNATURE 
      Ĵ
       cb2            
      

      The same strategy would hold true for adding things to the
      device specific dialog.

      The device can access its job properties with one pointer
      only.  Ex:

          PEPSONJOBPROPERTIES pEpJobProp;

          pEpJobProp->ul   // ulong data
          pEpJobProp->sz   // string data
          pEpJobProp->f    // boolean data

Functions that read/use job properties
  enable.c
     OS2_PM_DRV_DEVMODE
  helper.c
     SetDriverDataDefaults
     ValidateDriverData
     ReadJobProperties

  - Test network print scenario where a backlevel (old version)
    OMNI driver on the client prints to the current level OMNI
    driver on the server.  Also, test a network print scenario
    where a backlevel OMNI driver on the server receives a print
    job from the current level OMNI driver on a client.

The signature is a validation marker (4 bytes / 1 ULONG / usually a
character string / ex: see #define DDC_SIG, DB_SIG) that will tell
the device that this job properties is valid for it.  So an Epson
device upon seeing the HP device's job properties will ignore it and
overwrite it with valid Epson job properties.

The signature is also viewed with the device ID.  That is the
signature provides a driver level determination and the device id
provides a device level determination.

Also, the code to report the size of the new job properties needs to
loop through of the devices in the omni driver and take the maximum
value and add it to the OMNI drivers job property size.
