One of my favorite and least favorite classes in DCL is DCLWhen I need to work with text files I usually turn to DCL_System_IO_AsciiFile, an updated version of Konrad Byers' AnyAsciiFileClass originally published in Clarion Magazine. . ItBut there's one of my favorites because it's so useful; it's one of my least favorites because there's a very inelegant bit of code needed to start using it.
Shortly after I began using AnyAsciiFileClass I ran into a situation where I needed multiple instances of the class; I needed
One of the changes I made to the class was to add support for multiple instances. Originally, AnyAsciiFileClass contained a single file definition declared in the class's CLW file:
Code Block |
---|
AnyAsciiFile FILE,DRIVER('ASCII'),CREATE,NAME(AnyAsciiFileName),PRE(AsciiFile)
RECORD
TextLine STRING(ANYASCII_IO_SIZE)
END
END |
The problem is that data declared in a class module is shared by all instances of the class.thing about the class that drives me up the wall: it needs some clunky initialization code.
It wasn't always that way - the original class was much more intuitive.Things started to get ugly when I needed to work with two text files at the same time.
I tried declaring two instances of the class, but the code didn't work. That's because the class contained only one ASCII file declaration, and no matter how many instances of the class I created there was still just one ASCII file. Multiple classes using the same file declaration for operations on multiple text files is a recipe for disaster.
By way of example, here is a simple class I've called DemoClass. This is the .INC file:
...
You can see that the CLW file contains a declaration of a LONG variable, and the declared outside the class but inside the class's source module. The one method in the class returns the address of this variable.
Now hereHere's a small program to prove that SomeClassModuleData is the same in all instances of DemoClass:
Code Block |
---|
PROGRAM
MAP
END
include('DemoClass.inc'),once
InstanceA DemoClass
InstanceB DemoClass
CODE
if InstanceA.GetClassModuleDataAddress() = InstanceB.GetClassModuleDataAddress()
message('Class module data address is the same for both instances')
else
message('Class module data address is different for each instance')
end |
The result of running the program:
Getting
back to AnyAsciiFileClass, when I tried to use multiple instances I had problems because each instance was operating on the same file buffer, using the same file nameYou can see why I had a problem with multiple instances of AnyAsciiFileClass, What I needed was one file structure for each instance of the class. Only it isn't possible to declare a file layout inside a class. Files have to be declared at compile time; you can't create the on the fly at runtime (at least not without the Dynamic File Driver or DynaLib).
I created to two workarounds. One was to pass in an external file layout . That (which worked but required , but meant I had to come up with a new Ascii file declaration layout each time. The ); the other was to create some internal file layouts, with corresponding class instances using those layouts, and specify which one I wanted. This a predetermined number of file layouts and have some sort of mechanism for assigning a layout to a given instance. I'm not sure that was any improvement, as it resulted in the following clunky code:
Code Block |
---|
! Declaration TestFile &DCL_System_IO_AsciiFile code TestFile &= DCL_System_IO_AsciiFileManager.GetAsciiFileInstance(DCL_System_IO_AsciiFile_InstanceNumber1) |
This is really pretty awful stuff. First of all I have had to declare the file object as a reference rather than an instance. And how is anyone supposed to know they're to use this mysterious DCL_System_IO_AsciiFileManager object, not to mention that ridiculous instance number equate. And what's to prevent someone from using the same instance number twice?
All I should really need is really wanted to be able to use the class the same way it was originally designed, only I wanted it to work for multiple instances:
Code Block |
---|
TestFile DCL_System_IO_AsciiFile AnotherTestFile DCL_System_IO_AsciiFile ! etc |
There's no reason this can't work, and in fact the secret is to use the same feature that caused the problem: anything declared in the class module file is shared among all instances. That's where the instance management code needs to go.
...
Pool management
I decided to treat this as a pool management problem. I needed a list of all of the available file layouts, and a way to mark a file as in use. Then in the constructor I would check the list and initialize the class instance with an available file layout.
My first thought was to write this pool management code as a separate class. My second thought was that the code would be pretty simple, so I might as well just do it with a few procedures and a queue declared in the class source module. And my third thought, after about five minutes of coding, was that it would probably be best after all to write the code as a separate class, which is what I did.
Sometimes when I create a new class the name of the class is obvious; other times I agonize over my choice, often changing it later. I'm still mulling over this one. In any case, I decided to call it DCL_System_Pool, although it's not really an object pool, more of a reference number pool. But it can server as a basis for more involved pool implementations, so for now DCL_System_Pool it is. If you're reading this years later and you don't see a class by that name in the DCL, keep looking. It's probably there somewhere.
Here's the declaration:
Code Block |
---|
include('DCL_IncludeInAllClassHeaderFiles.inc'),once
!include('DCL_System_ErrorManager.inc'),once
include('DCL_System_Threading_CriticalSection.inc'),once
DCL_System_PoolItemQueue queue,type
ItemNumber long
InUse bool
end
DCL_System_Pool Class,Type,Module('DCL_System_Pool.CLW'),Link('DCL_System_Pool.CLW',_DCL_Classes_LinkMode_),Dll(_DCL_Classes_DllMode_)
!Errors &DCL_System_ErrorManager
AccessLock &DCL_System_Threading_CriticalSection
StopOnError bool,protected
StopOnErrorMessage cstring(500)
ItemQ &DCL_System_PoolItemQueue
Construct Procedure()
Destruct Procedure()
GetFreeItemCount procedure,long
GetItemNumber procedure,long
Init procedure(long maxItems)
ReleaseItemNumber procedure(long itemNumber)
SetStopOnError procedure(bool StopOnError,<string errorMessage>)
End |
and here's the method code:
Code Block |
---|
Member
Map
End
Include('DCL_System_Pool.inc'),Once
DCL_System_Pool.Construct Procedure()
code
self.ItemQ &= new DCL_System_PoolItemQueue
self.AccessLock &= new DCL_System_Threading_CriticalSection
self.StopOnErrorMessage = 'All pool items are in use'
DCL_System_Pool.Destruct Procedure()
code
!dispose(self.Errors)
free(self.ItemQ)
dispose(self.ItemQ)
dispose(self.AccessLock)
DCL_System_Pool.GetFreeItemCount procedure!,long
FreeItemCount long
x long
code
self.AccessLock.Wait()
loop x = 1 to records(self.ItemQ)
get(self.ItemQ,x)
if not self.ItemQ.InUse then FreeItemCount += 1.
end
self.AccessLock.Release()
return FreeItemCount
DCL_System_Pool.GetItemNumber procedure!,long
x long
retval long
code
self.AccessLock.Wait()
retval = -1
loop x = 1 to records(self.ItemQ)
get(self.ItemQ,x)
if not self.itemq.InUse
self.ItemQ.InUse = true
put(self.ItemQ)
retval = self.ItemQ.ItemNumber
break
end
end
self.AccessLock.Release()
if retval = -1 and self.StopOnError then Stop(self.StopOnErrorMessage).
return retval
DCL_System_Pool.Init procedure(long maxItems)
x long
code
self.AccessLock.Wait()
free(self.ItemQ)
loop x = 1 to maxItems
self.ItemQ.ItemNumber = x
self.ItemQ.InUse = false
add(self.ItemQ)
end
self.AccessLock.Release()
DCL_System_Pool.ReleaseItemNumber procedure(long itemNumber)
code
self.AccessLock.Wait()
self.ItemQ.ItemNumber = itemNumber
get(self.ItemQ,self.ItemQ.ItemNumber)
if not errorcode()
self.itemq.InUse = false
put(self.ItemQ)
end
self.AccessLock.Release()
DCL_System_Pool.SetStopOnError procedure(bool StopOnError,<string errorMessage>)
code
self.StopOnError = StopOnError
if not omitted(errorMessage) then self.StopOnErrorMessage = errorMessage. |
The logic is pretty easy to follow; when you get a pool element it's marked as in use; when you're done with it and release it the flag is cleared. There's also some support for customizing error handling. To make the class thread safe I've added a critical section.
Clearly even if this is the only place I ever use the class, having it in its own INC and CLW makes the class easier to work with and more visible within the DCL.
Inside DCL_System_IO_AsciiFileManager I added five file layouts:
Code Block |
---|
AsciiFileName1 STRING(MaxPathLength),static
AsciiFile1 FILE,DRIVER('ASCII','/CLIP = on'),CREATE,NAME(AsciiFileName1),PRE(AsciiFile1)
RECORD
Txt STRING(ASCII_IO_RECORD_SIZE)
END
END
AsciiFileName2 STRING(MaxPathLength),static
AsciiFile2 FILE,DRIVER('ASCII','/CLIP = on'),CREATE,NAME(AsciiFileName2),PRE(AsciiFile2)
RECORD
Txt STRING(ASCII_IO_RECORD_SIZE)
END
END
AsciiFileName3 STRING(MaxPathLength),static
AsciiFile3 FILE,DRIVER('ASCII','/CLIP = on'),CREATE,NAME(AsciiFileName3),PRE(AsciiFile3)
RECORD
Txt STRING(ASCII_IO_RECORD_SIZE)
END
END
AsciiFileName4 STRING(MaxPathLength),static
AsciiFile4 FILE,DRIVER('ASCII','/CLIP = on'),CREATE,NAME(AsciiFileName4),PRE(AsciiFile4)
RECORD
Txt STRING(ASCII_IO_RECORD_SIZE)
END
END
AsciiFileName5 STRING(MaxPathLength),static
AsciiFile5 FILE,DRIVER('ASCII','/CLIP = on'),CREATE,NAME(AsciiFileName5),PRE(AsciiFile5)
RECORD
Txt STRING(ASCII_IO_RECORD_SIZE)
END
END |
I also added a class derived from DCL_System_Pool. This class has a constructor that initializes the pool object with five elements, one for each of five layouts:
Code Block |
---|
AsciiFilePool class(DCL_System_Pool)
Construct procedure
end
AsciiFilePool.Construct procedure
code
self.Init(5)
self.SetStopOnError(true,'DCL_System_IO_AsciiFile: All available ASCII files are in use!') |
Finally I added some code to the DCL_System_IO_AsciiFile constructor to get the first available ASCII file (via the pool object) and then one line to the destructor to release the ASCII file.
Code Block |
---|
DCL_System_IO_AsciiFile.Construct PROCEDURE()
CODE
self.Errors &= new DCL_System_ErrorManager
self.PoolItemNumber = AsciiFilePool.GetItemNumber()
execute self.PoolItemNumber
self.Init(AsciiFile1,AsciiFile1:Record,AsciiFileName1)
self.Init(AsciiFile2,AsciiFile2:Record,AsciiFileName2)
self.Init(AsciiFile3,AsciiFile3:Record,AsciiFileName3)
self.Init(AsciiFile4,AsciiFile4:Record,AsciiFileName4)
self.Init(AsciiFile5,AsciiFile5:Record,AsciiFileName5)
else
stop('Unable to initialize the ASCIIFile instance - if you continue the program will probably crash')
end
DCL_System_IO_AsciiFile.Destruct PROCEDURE()
CODE
SELF.CloseFile()
AsciiFilePool.ReleaseItemNumber(self.PoolItemNumber)
dispose(self.Errors)
|
I've updated the GitHub repository with these changes. If you previously used the ASCIIFileManager approach then you'll need to make two simple changes:
- Remove the ASCIIFileManager call
- Most importantly, remove the leading & from your DCL_System_IO_AsciiFile declaration to change it from a reference to an instance.
I wish I'd made this change sooner - the refactoring wasn't all that difficult, the class is much, much easier to use, and I've added a new pool tool to the kit in the event I need similar functionality somewhere else.
Note | ||||
---|---|---|---|---|
| ||||
After I posted this article and a shorter one titled Class exporter now a pre-build task (DCL) I decided I needed to deal with a circular reference problem in the class exporter, which now uses DCL_System_IO_AsciiFile to read its list of class headers to parse from a text file. Originally CreateDCLExportFile made use of DevRoadmapsClarion.DLL, but because CreateDCLExportFile is now a pre-build task for the class library there's a risk that a change to the class library could cause a runtime failure which would break the class library build. With the build broken, how do I then create the EXP so the build can succeed? Big problem. But wait, that's no problem at all. I can do the same thing with CreateDCLExportFile that I did with ClarionTest - compile in the required classes so I don't need the DLL. Brilliant! Except when I did that, CreateDCLExportFile GPFd. I actually resorted to the debugger to see what was the problem. And I found that in an EXE my AsciiFilePool class was not being instantiated. Why, I can't imagine. The fix was to change AsciiFilePool to a typed class and set up a reference:
The first line of my DCL_System_IO_AsciiFile.Construct method now has this line:
This way I'm guaranteed to have a pool instance. I find it quite strange that a module-level instance should need to be instantiated for EXEs but not DLLs, but there it is. If anyone can shed light on this please do so. |
Info | ||
---|---|---|
| ||
After further investigation I discovered the reason for the GPF. It wasn't that the pool instance wasn't created, it was that in the EXE the pool instance was created after the file class instance. That meant the file class constructor fired before it had a pool instance to work with. In the DLL however the pool instance was created first. I'm not sure why that should be the case; perhaps in the EXE anything in declared the module is only created when the module itself comes into scope (as when the class is instantiated), while in the DLL all static declarations are automatically instantiated on DLL loading. But that's just speculation on my part. In any case, taking control of the pool object's instantiation solves the problem. |