[Okta Webinar] Learn how to a build a cloud-first strategyRegister Now

x
  • Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 531
  • Last Modified:

Delphi, COM and Access Violation calling object methods from an interface instance

Windows Vista Ultimate, 64bit, IE 7, Borland Developer Studio 2006


Ran into an interesting problem. I'm not sure what this falls under,
but I did some researching already and can't find anything that seems
to fix the problem.

I've got a class defined as:

  TDynamicNS = class(TComObject, IInternetProtocol)
    function Start(szUrl: PWideChar; OIProtSink:
IInternetProtocolSink; OIBindInfo: IInternetBindInfo; grfPI,
dwReserved: DWORD): HResult; stdcall;
    function Continue(const ProtocolData: TProtocolData): HResult;
stdcall;
    function Abort(hrReason: HResult; dwOptions: DWORD): HResult;
stdcall;
    function Terminate(dwOptions: DWORD): HResult; stdcall;
    function Suspend: HResult; stdcall;
    function Resume: HResult; stdcall;
    function Read(pv: Pointer; cb: ULONG; out cbRead: ULONG): HResult;
stdcall;
    function Seek(dlibMove: LARGE_INTEGER; dwOrigin: DWORD; out
libNewPosition: ULARGE_INTEGER): HResult; stdcall;
    function LockRequest(dwOptions: DWORD): HResult; stdcall;
    function UnlockRequest: HResult; stdcall;
  end;

In the events Start(), Read(), and Terminate(), I have code that is
checking a global record in another unit for an assigned callback.
When I do this, I get an access violation. I'm not sure I'd need to do
any sort of marshalling, as I'm not passing the interface, referencing
it, or doing anything with it. Here's an example of what's going on:

type
  TOnRead = procedure(const AEvent: Integer);

  TGlobalData = record
    OnRead: TOnRead;
  end;

var
  gGlobalData: TGlobalData;

function TDynamicNS.Read(pv: Pointer; cb: ULONG; out cbRead: ULONG):
HResult; stdcall;
begin
  if (Assigned(gGlobalData.OnRead)) then
    gGlobalData.OnRead(0);
end;

The gGlobalData.OnRead event is assigned by another class:
  TMyClass = class(TObject)
  private
    FLastEvent: Integer;
  public
    procedure OnRead(const AEvent: Integer);
  end;

procedure TMyClass.OnRead(const AEvent: Integer);
begin
  FLastEvent := AEvent; //<-- AV here
end;

I know the AV is related to calling the class from the interface.. but
I'm not sure why, or how to fix it. Anyone have any tips? I have
experience in marshalling, but I've never done it with something that
has been defined to classes like this.
0
akede2001
Asked:
akede2001
  • 8
  • 4
  • 4
1 Solution
 
akede2001Author Commented:
This may actually be a marshaling problem, but what throws me off is the class definition:

TDynamicNS = class(TComObject, IInternetProtocol)

If that is the case, how do I marshal this, so that I can call class object methods on the main thread? In the end, what I'm doing is in the OnRead, OnTerminate, and OnStart callbacks is to write the data to a file. In the case of all three events, I'm updating a single enumeration in a class which is created in the main thread.
0
 
ziolkoCommented:
why don't you create property OnRead in TDynamicNS:

TOnReadEvent = procedure(AEvent: Integer) of object;

TDynamicNS = class(...)
...
private
  FOnRead: TOnReadEvent;
...
public
...
property OnRead: TOnReadEvent read FOnRead write FOnRead;


...

function TDynamicNS.Read(pv: Pointer; cb: ULONG; out cbRead: ULONG):
HResult; stdcall;
begin
  if (Assigned(FOnRead)) then
    FOnRead(...);
end;

ziolko.
0
 
akede2001Author Commented:
I cannot create a property for it on TDynamicNS because it's an interface. It's being called asynchronously. TDynamicNS is being employed as a pluggable protocol. When it calls events such as Start, Read, and Terminate, I update another class so I can track activity and look for "silent" periods where there is no activity.

The actual event where I need to do something is controlled by another class (TMyClass, which only runs one instance at any given time). When an instance of TDynamicNS is created, if I assign it a property and pass it TmyClass.OnRead, then the access violation occurs still.
0
Technology Partners: We Want Your Opinion!

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

 
ziolkoCommented:
>I cannot create a property for it on TDynamicNS because it's an interface
it depends how it's being created, in some cases you can expand it to support your events, post some code how you create instace of TDynamicNS

in second issue correct me if i misunderstood, you have only one instance of TMyClass  and multiple instances of TDynamicNS can be created at any time and spawn Read event, if this is the case it's very likely that you need some synchronization.

ziolko.
0
 
akede2001Author Commented:
In the unit where TDynamicNS is defined and implemented:

initialization
  TComObjectFactory.Create(ComServer, TDynamicNS, Class_DynamicNS, 'DynamicNS', 'DynamicNS', ciMultiInstance);//, tmApartment);



Main form..
TfrmMain - private
    FFactory: IClassFactory;
    FInternetSession: IInternetSession;


procedure TfrmMain.FormCreate(Sender: TObject);
begin
  CoGetClassObject(Class_DynamicNS, CLSCTX_SERVER, nil, IClassFactory, FFactory);
  CoInternetGetSession(0, FInternetSession, 0);

  FInternetSession.RegisterNameSpace(FFactory, Class_DynamicNS, 'http', 0, nil, 0);
end;

Every time the IE browser in my application tries to download something, a new DynamicNS is created. So when I do a .Navigate() on the browser on my form, I want to intercept the data being downloaded. All of that works fine. When i get the Start() event through the interface, I go and download the requested URL myself. Recently, I've wanted to keep track of activity.. so when Start() is called, I attempt to set an event through a class object method created in the main thread, via TMyClass. That's where I get the error. I'm fine with multiple instances on TDynamicNS, as I'd prefer the documents to download as quickly as possible.

As for the second issue and a correct, I do only have one instance of TMyClass. However, the Read() event is an interface routine and can be called multiple times. When it is called, I fire an event in my own program which I've named OnRead, which handles the update with TMyClass.

So, basically:

TDynamicNS.read gets called asynchronously, from outside of my program.
TDynamicNS.read checks a global record for OnRead, if assigned, calls it

The global record OnRead value is TMyClass.onRead, so effectively:

TDynamicNS.read checks a global record for OnRead, if assigned, calls TmyClass.onRead.

If adding some synchronization works, I'll go with that. But do keep in mind that TDynamicNS is a wrapper around an interface that works asynchronously. If synchronization is limited only to the TMyClass.OnRead(...), then that's no problem as it would not impede on download speed. However, if synchronization would only allow one download at a time, then I cannot use that and would need to seek a solution that is also asynchronous. How would you suggest implementing the synchronization, if possible?
0
 
ziolkoCommented:
ok i got you know, and i think it's synchor problem.
will synchornization limit number of download speed - no, concept of sunchronization is to protect resources that can be accessed simultaniously by more that one thread/process in your case:
procedure TMyClass.OnRead(const AEvent: Integer);
begin
  FLastEvent := AEvent; //<-- AV here
end;

here's problem because two different threads might want to change FLastEvent in same time, so you need synchronize this part.
add SyncObjs to your uses clausule
expand your TMyClass:

TMyClass = class(...)
private
  FCriticalSection: TCriticalSection;
...
public
  construcotr Create;
  destrucotr Destroy;override;
  procedure OnRead(AEvent: Integer);
...
end;

constructor TMyClass.Create;
begin
  inherited Create;
  FCriticalSection := TCriticalSection.Create;
end;

destructor TmyClass.Destroy;
begin
  FreeAndNil(FCriticalSectoin);
  inherited Destroy;
end;

procedure TMyClass.OnRead(AEvent: Integer);
begin
  FCriticalSection.Enter;
  try
    FLastEvent := AEvent;
  finally
    FCriticalSection.Leave;
  end;
end;

ziolko.
0
 
akede2001Author Commented:
Throwing in some TCriticalSection objects isn't going to prevent the access violation, though. I can step through with the debugger, and the instant it flows from TDynamicNS.Start() to TMyClass.OnRead() and attempts to modify anything about TMyClass via TMyClass.OnRead(), there's an access violation.

As for the access violation, I've confirmed that TMyClass is indeed created successfully. The problem appears to be that TMyClass is created in the main application thread. TDynamicNS is an interface object created via COM, so it does not belong to the main application thread. It's essential one thread trying to modify another thread created in a different context.

I did implement the CriticalSection routines as suggested. The access violation still occurs. Do you know of an easy way to marshal the TDynamicNS interfaced object and unmarshal it in the main thread so that TMyClass can be called and changed, and have the interfaced object TDynamicNS and standard class TMyClass viewed as being in the same context/thread?

0
 
akede2001Author Commented:
When I make the TMyClass instance global by creating it in an initialization value, and setting a global variable in the interface section, I can call it and make changes to it.

However, if it is not global, it doesn't work. Meaning, if I create an instance of TMyClass in the main thread, say frmMain.OnCreate() and assign the address of TMyClass to a globally available record.. then call it from there, it does not work. For example:

interface
...
var
  gMyClass: TMyClass;
initialization
  gMyClass := TMyClass.Create();
finalization
  FreeAndNil(gMyClass);

I can call gMyClass.OnRead(...) from TDynamicNS.Read(...) without a problem. However, if I change it so gMyClass is created within the context of a thread then it doesn't work. For example:

interface
  gMyThread: TMyClass;

frmMain.OnCreate(...):
  gMyThread := TMyClass.Create();


I can create it fine, but I get an access violation now when TDynamicNS tries to call it. It looks like I will have to marshal TDynamicNS and/or TMyClass so they can operate on each other. TDynamicNS does not own TMyClass, and is in a different thread context than TMyClass. TMyClass belongs to my application. TDynamicNS is a COM interface, which needs to appear to belong to my application before it can do things with classes created within my application's context. Any suggestions on how to go about doing that?
0
 
ziolkoCommented:
hmm funny, i've done pretty big piece of code with WMI (DCOM) interfaces but never bumped into such AV.

when you stop on breakpoint in: FLastEvent := AEvent; can you see both values in variable watch?

maybe not smartest idea, but if it's about only one integer try PostMessage()

ziolko.
0
 
developmentguruCommented:
Delphi COM 101...

  Try to check the usage count.  When the usage count reaches 0 a COM object will free itself.  Once that happens any remaining references to the COM object will, throw exceptions when you try to access it (since it no longer exists).  You can put a breakpoint in the destroy method of your class.  If it breaks on that before your thread tries to access it, it will verify that this is the issue.  I ran into this problem when I went to work at an insurance company.  They were handing out "Runtime Error 216" T-Shirts shortly before I got there.  They had code in their COM objects like this... (if you have a weak stomach, stop reading here)

  While MyCOMObject.RefCount > 2 do
   MyCOMObject.Release;

  And a few other such attrocities.  In general Delphi handles the reference count for you and if you start massaging the reference counter you will most likely wind up with errors.

  Now, I am not saying this is the case with you.  When you call your thread, it needs to have a local reference to the COM object.  If it does not then, when you pass out of the code that created the COM object Delphi will Release the one reference to it and it will destroy itself.  By passing it to a local variable in your COM object the referecne count will go to 2.  Then when the main application code that created the COM object exits it will still Release it (reducing the count to 1) and the COM object will still be viable.

Let ne know if this helps.
0
 
akede2001Author Commented:
developmentguru:

The instances of TDynamicNS are not created within a new thread. It's the asynchronous interface that gets called via IInternetProtocol. When a method is called, I want to update an integer in a standard class defined as:

TMyClass = class(TObject)

The class TMyClass is in another unit, and only runs one instance. The instance is created by my main form's OnCreate() event. When TMyClass is created, I assign a pointer to it in a globally available record so that TDynamicNS can see it, and access it. This is then where the error happens, when TDynamicNS tries to do something with the TMyClass reference that was created by frmMain's implementation area.


It works when I create TMyClass and throw a global reference to it in a unit, create it in the unit's initialization section, then call it by that global variable. If I create it in the implementation section of a unit, in something like frmMain, then I get access violations when trying to do anything with TDynamicNS.
0
 
developmentguruCommented:
 Instead of having the object available globally, pass a reference to your TMyClass instance to your thread object (either in the constructor or as a property, etc).  You will then need to be sure that only one piece of code is allowed to access it at a time (mutex, semaphore, etc).  This will also have some other effects... When you pass the object reference you will be able to verify that it is valid, and that the reference inside the class is valid.  From that point you should only need to be certain that the class does not get destroyed before being used again.

  I have handled this type of scenario in quite a few situations and had no problem with it as lkong as 1) you can be certain that only one piece of code accesses it at one time, and 2) the object remains valid.  Make sure to create a destructor for your TMyClass if it does not have one since you will need to be able to place a breakpoint there so you can see when it is freed.

  if none of that helps... I do have another idea.  If your code is being called from different processes then the error is simple that your variable is not accessible outside your process.  You can fix that by using the type of code shown below.  The area of memory can then be accessed by different processes requesting it by name.  This will make it extra important to uses some means of restricting access to a single instance at a time.
------------------------------------------------------------------------------
var
  FileMap : THandle;
  PCount : ^integer;

procedure MapCounter;
begin
  FileMap := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0,
    SizeOf(integer), 'MyNamedSpace');
  PCount := MapViewOfFile(FileMap, FILE_MAP_WRITE, 0, 0, 0);
  PCount^ := 0; //initialize the count
end.

procedure UnMapCounter;
begin
  UnMapViewOfFile(PPrevious);
end;
------------------------------------------------------------------------------

If you are using this from multiple instances it might actually be easier to do it from a singleton COM object instead.

Let me know...
0
 
akede2001Author Commented:
developmentguru:

Can you explain the CreateFileMapping method in a bit more detail, please? I think that's the route I'll be going.  I've already changed the code to be globally available, but as soon as I expand on it in the future for multiple instances, that method isn't really going to work anymore. I've implemented the TCriticalSection objects already to restrict access; so setting it up to use mapped writes shouldn't be a problem, from the sounds of it.

With the mapped file view, how would I read/write to it? In the MapCount() that you provided, you initialized a PCount, and in UnMapCount there's an unknown PPrevious. Do you happen to have a small working example of this? It doesn't need any COM interfacing or anything, basically just the create, destroy, read, and write.
0
 
developmentguruCommented:
 The PPrevious should have read as PCount.  Sorry, I tried to rewrite something I had done on the fly.  The CreateFileMapping is simply creating a section of memory that is available by name.  This means that you can use it across instances.  I have used it to create code to enforce an application only runs a single instance.  In this context it would write the windows main handle to the shared memory.  Then, when a second instance tries to run it can read the handle of the original window and set focus to it before exiting.  Here is a link to the details.

http://msdn2.microsoft.com/EN-US/library/aa366537.aspx

The function returns the handle even if it has already been created.  You can tell if it has already been created by checking GetLastError for the value ERROR_ALREADY_EXISTS.

If you would like I can show the code I used for application single instance.  Let me know.
0
 
akede2001Author Commented:
developmentguru:

If you could provide the code, that would be great. I've since worked around the problem by going global, but I'll reward you the full points for the sample code with file mapping, as I know that would also work by seeing used in other situations related to COM programming.
0
 
developmentguruCommented:
This is the code that I use the application single instance.

------------------------------------------------------------------------
program XXXXXX;

uses
  ...;

var
  FileMap, Handle : THandle;
  PPrevious : PHandle;
  FirstInstance : boolean;

begin
  FileMap := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0,
    SizeOf(THandle), 'XXXXXX single instance');
  FirstInstance := GetLastError <> ERROR_ALREADY_EXISTS;

  PPrevious := MapViewOfFile(FileMap, FILE_MAP_WRITE, 0, 0, 0);
  try
    if not FirstInstance then
      begin
        Handle := PPrevious^;

        SetForegroundWindow(Handle);
      end
    else
      begin //successfully created, this is first instance.
        Application.Initialize;
        Application.Title := 'Polydeck Proposal & Order Viewer (Wizard2)';
        Application.CreateForm(TdmWizard2, dmWizard2);
  Application.CreateForm(TfMain, fMain);
  Application.CreateForm(TfItemHistory, fItemHistory);
  PPrevious^ := fMain.Handle;

        Application.Run;
      end;
  finally
    UnMapViewOfFile(PPrevious);
  end;
end.
------------------------------------------------------------------------
Now you can see where the PPrevious came into play... hope this helps you.
0

Featured Post

Independent Software Vendors: We Want Your Opinion

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

  • 8
  • 4
  • 4
Tackle projects and never again get stuck behind a technical roadblock.
Join Now