I wrote a simple console program to cast some RTTI magic:
program TypeCast;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, System.RTTI, Generics.Collections;
type
TSpr = class
public
s: string;
i: Integer;
b: Boolean;
end;
var
Spr: TSpr;
vCtx: TRTTIContext;
vType: TRTTIType;
vField: TRTTIField;
Dict: TDictionary<string, TRTTIField>;
begin
try
Spr := TSpr.Create;
vType := vCtx.GetType(TSpr.ClassInfo);
Dict := TDictionary<string, TRTTIField>.Create;
for vField in vType.GetFields do
Dict.AddOrSetValue(vField.Name, vField);
Dict['s'].SetValue(Spr, 'Hello World!');
Dict['i'].SetValue(Spr, 123);
Dict['b'].SetValue(Spr, True);
Writeln(Spr.s);
Writeln(Spr.i);
Writeln(Spr.b);
Spr.Free;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
Output:
Hello World!
123
TRUE
If I compile and run this program, it works fine. But if I use such technique to forward variables of these types to object, defined in another package, it gets me into lot of troubles.
MCVE stuff / Steps To Reproduce:
...assuming you are starting from empty environment...
Create PluginInterface package. Add there UClassManager
unit UClassManager;
interface
uses
Classes, Contnrs;
type
TClassManager = class(TClassList);
function ClassManager: TClassManager;
implementation
var
Manager: TClassManager;
function ClassManager: TClassManager;
begin
Result := Manager;
end;
initialization
Manager := TClassManager.Create;
finalization
Manager.Free;
end.
and UPlugin units.
unit UPlugin;
interface
uses RTTI;
type
TPlugin = class
public
procedure Init; virtual; abstract;
function SetProp(Key: string; Value: TValue): Boolean; virtual; abstract;
end;
TPluginClass = class of TPlugin;
IPluginHost = interface
function RunPlugin(PluginName: string): TPlugin; // Run Plugin by it's ClassName
end;
var
Host: IPluginHost;
implementation
end.
Create VCL Forms Application, enable runtime packages, add reference to PluginInterface and add TButton onto it. Make these handlers for corresponding events:
procedure TForm1.FormCreate(Sender: TObject);
begin
LoadPackage('UniversalSpr.bpl');
Host := Self;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Plugin: TPlugin;
begin
Plugin := Host.RunPlugin('TSprPlugin');
Plugin.Init;
Plugin.SetProp('SprTableName', 'MATERIALS');
Plugin.SetProp('EditRights', True);
Plugin.SetProp('BeforePostValue1', 3);
end;
function TForm1.RunPlugin(PluginName: string): TPlugin;
var
I: Integer;
begin
Result := nil;
for I := 0 to ClassManager.Count - 1 do
if ClassManager[I].ClassNameIs(PluginName) then begin
Result := TPluginClass(ClassManager[I]).Create;
Break;
end;
end;
Of coarse, TForm1 should be descendant of IPluginHost. And don't forget to add UClassManager and UPlugin into uses clause. Other units will be added automatically by IDE.
Create package UniversalSpr and place it's output file into the same directory where your application is placed. Implement UPlugin inside TSprPlugin:
unit USprPlugin;
interface
uses
UPlugin, RTTI, Generics.Collections;
type
TSpr = class
SprTableName: string;
BeforePostValue1: int64;
EditRights: boolean;
end;
TSprPlugin = class(TPlugin)
procedure Init; override;
function SetProp(Key: string; Value: TValue): Boolean; override;
private
Spr: TSpr;
PropDict: TDictionary<string, TRTTIField>;
end;
implementation
procedure TSprPlugin.Init;
var
vCtx: TRTTIContext;
vType: TRTTIType;
vField: TRTTIField;
begin
if not Assigned(Spr) then
Spr := TSpr.Create;
vType := vCtx.GetType(Spr.ClassInfo);
if not Assigned(PropDict) then
PropDict := TDictionary<string, TRTTIField>.Create;
for vField in vType.GetFields do
PropDict.Add(vField.Name, vField);
end;
function TSprPlugin.SetProp(Key: string; Value: TValue): Boolean;
var
Field: TRTTIField;
begin
Result := PropDict.TryGetValue(Key, Field);
if Result then
// here I get Access Violation
Field.SetValue(Spr, Value);
end;
end.
After you click Button1, you can pass specified values into properties setter/mutator, but if you try to repeat my TypeCast trick inside the routine, you'll get Access Violation trying to access 00000004.
Also, investigation and advanced debugging shows that Field.FieldType evaluates correctly (which explains why InsufficientRTTI is not thrown), but if I want to get Field.Fieldtype.Handle, I get the infamous AV.
I can set the value just skipping Cast from original SetValue method:
function TSprPlugin.SetProp(Key: string; Value: TValue): Boolean;
var
Field: TRTTIField;
begin
Result := PropDict.TryGetValue(Key, Field);
if Result then
Value.ExtractRawData(PByte(Spr) + Field.Offset);
end;
Dalija recommended to avoid packages, I already took this into accout, that's why I created TypeCast to test RTTI. But I need packages, because of design of my application, I cannot just rewrite it to be monolythic. What can I do to avoid this Access Violation without abandoning packages?
Your current code has some issues regardless whether you use runtime packages or not. Your MCVE is not exactly minimal, and you have added too many steps from your working console application to your packaged code that does not work.
In debugging your issue you should have started from encapsulating logic into TSprPlugin class and testing that class directly without messing with runtime packages. When you are sure that TSprPlugin code functions properly, then you can add packages and see how it goes.
Right now your code fails with following simple test project
program test;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
USprPlugin in 'USprPlugin.pas',
UPlugin in 'UPlugin.pas';
var
Plugin: TPlugin;
begin
Plugin := TSprPlugin.Create;
Plugin.Init;
Plugin.SetProp('SprTableName', 'MATERIALS');
Plugin.SetProp('EditRights', True);
Plugin.SetProp('BeforePostValue1', 3);
end.
Moving vCtx from local variable to TSprPlugin field solves the issue for above test case.
unit USprPlugin;
interface
uses
UPlugin, RTTI, UniversalSprUnit, Generics.Collections;
type
TSprPlugin = class(TPlugin)
vCtx: TRTTIContext;
procedure Init; override;
function SetProp(Key: string; Value: TValue): Boolean; override;
private
Spr: TSpr;
PropDict: TDictionary<string, TRTTIField>;
end;
implementation
procedure TSprPlugin.Init;
var
vType: TRTTIType;
vField: TRTTIField;
begin
vCtx := TRttiContext.Create;
if not Assigned(Spr) then
Spr := TSpr.Create;
vType := vCtx.GetType(Spr.ClassInfo);
if not Assigned(PropDict) then
PropDict := TDictionary<string, TRTTIField>.Create;
for vField in vType.GetFields do
PropDict.Add(vField.Name, vField);
end;
function TSprPlugin.SetProp(Key: string; Value: TValue): Boolean;
var
Field: TRTTIField;
begin
Result := PropDict.TryGetValue(Key, Field);
if Result then
// here I get Access Violation
Field.SetValue(Spr, Value);
end;
end.
Starting from there you can add up additional functionality step by step ensuring that each step didn't break functionality.
Also, you are not releasing Spr and PropDict fields thus creating memory leak, but I am not sure if that code is not included just because it is not directly connected with issues you are having, or you are really having memory leak there.
Related
I have a class TMyClass, on which I inject interface IFileManager. In this interface there is a method GetCompanyWorkbook(const ACompanyId: System.Integer; const AStream: TStream). This method fills AStream depend from ACompanyId. Everything works fine on real code, but when I run unit tests for class TMyClass and mocked IFileManager via framework Spring4D, FastMM reports for memory leak 13-20 bytes: TIndexWrapper x 1. I used last Spring4D version 1.26 from repository(branch main/master)
unit Unit1.pas
interface
DUnitX.TestFramework,
Spring.Mocking;
type
IFileManager = interface (IInvokable)
procedure GetCompanyWorkbook(const ACompanyId: System.Integer; const AStream: TStream);
end;
TMyClass = class
strict private
FFileManager: IFileManager;
public
constructor Create(const AFileManager: IFileManager);
procedure GenerateInvoice(const ACompanyId: System.Integer);
end;
[TestFixture]
TMyClassTests = class
strict private
FMockStream: TStream;
FMyClass: TMyClass;
FFileManager: Mock<IFileManager>;
procedure SetupFileManagerMock();
procedure InitMockStream(const AMockFile: string);
public
[Setup]
procedure Setup();
[TearDown]
procedure TearDown();
[TestCase('Test invoice generation', '2|invoice_2023.xls', '|')]
procedure TestGenerateInvoice(const ACompanyId: System.Integer; const AMockFile: string);
end;
implementation
uses
System.Classes,
Spring;
constructor TMyClass.Create(const AFileManager: IFileManager);
begin
Guard.CheckNotNull(AFileManager, 'AFileManager');
inherited Create();
Self.FFileManager := AFileManager;
end;
procedure TMyClass.GenerateInvoice(const ACompanyId: System.Integer);
begin
var sTmpFile := Self.GetTempInvoiceFile(ACompanyId);
var fs := TFileStream.Create(sTmpFile, fmCreate);
try
Self.FFileManager.GetComparyWorkbook(ACompanyId, fs);
// Do some operations with stream
finally
fs.Free();
end;
end;
procedure TMyClassTests.Setup();
begin
Self.FMockStream := nil;
Self.FMyClass := TMyClass.Create(Self.FFileManager);
end;
procedure TMyClassTests.TearDown();
begin
Self.FMyClass.Free();
Self.FMockStream.Free();
end;
procedure TMyClassTests.InitMockStream(const AMockFile: string);
begin
Self.FMockStream := TFileStream.Create(AMockFile, fmOpenRead);
end;
procedure TMyClassTests.SetupFileManagerMock();
begin
Self.FFileManager.Setup.Executes(
function(const callInfo: TCallInfo): TValue
begin
callInfo.Args[1].AsType<TStream>.CopyFrom(Self.FMockStream);
end)
.When(Args.Any)
.GetCompanyWorkbook(Arg.IsAny<System.Integer>, Arg.IsAny<TStream>);
end;
procedure TMyClassTests.TestGenerateInvoice(const ACompanyId: System.Integer; const AMockFile: string);
begin
Self.InitMockStream(AMockFile);
Self.SetupFileManagerMock();
Assert.WillNotRaiseAny(
procedure
begin
Self.FMyClass.GenerateInvoice(ACompanyId);
end
);
end;
The issue is that you are using this construct which is redundant:
.When(Args.Any)
.GetCompanyWorkbook(Arg.IsAny<System.Integer>, Arg.IsAny<TStream>);
Either pass Args.Any to When or use individual Arg matching on the parameters.
Passing Args.Any causes the mock internally to ignore the individual parameter matching. That causes the temporarily constructed object for the parameter matching to be leaked which is not trivial to be fixed.
Update: I was able to fix the memory leak in develop branch
I'm trying to create a minimal example, that does the same thing as the Spring4D [inject] Attribute. It's supposed to automatically resolve my TOrderAdapter.FDetailsAdapter, which I want to manually instantiate inside a Factory unit (not like the Spring4D container works, registering interfaces from the outside first). The Factory should hand out any desired interfaces requested with [inject].
It is pretty obvious that the code I have can not work (TOrderAdapter.FDetailsAdapter not being injected, giving me a nil pointer Access Violation on ButtonClick, the first use). Reading through the Spring4D source, I fail to see where this logical piece is, that I'm missing for the desired functionality to work in my example.
program OrderDetails;
uses
Vcl.Forms,
Order.Adapter in 'Order.Adapter.pas',
Details in 'Details.pas',
Details.Adapter in 'Details.Adapter.pas',
Factory.Adapter in 'Factory.Adapter.pas',
Factory in 'Factory.pas',
Order in 'Order.pas',
Order.View in 'Order.View.pas' {OrderForm};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TOrderForm, OrderForm);
Factory.Adapter.Factory := TFactoryAdapter.Create;
Application.Run;
end.
unit Factory;
uses
Rtti, TypInfo;
type
InjectAttribute = class(TCustomAttribute)
private
fServiceType: PTypeInfo;
fValue: TValue;
public
constructor Create(ServiceType: PTypeInfo); overload;
property ServiceType: PTypeInfo read fServiceType;
property Value: TValue read fValue;
end;
implementation
constructor InjectAttribute.Create(ServiceType: PTypeInfo);
begin
inherited Create;
fServiceType := ServiceType;
end;
end.
unit Factory.Adapter;
uses
Details, Details.Adapter, Order, Order.Adapter;
type
TFactoryAdapter = class
private
FDetailsAdapter: IDetailsAdapter;
FOrderAdapter: IOrderAdapter;
public
constructor Create;
function Inject: IInterface; overload; // unused
end;
var
Factory: TFactoryAdapter;
implementation
constructor TFactoryAdapter.Create;
begin
FDetailsAdapter := TDetailsAdapter.Create;
FOrderAdapter := TOrderAdapter.Create;
end;
function TFactoryAdapter.Inject: IInterface; // unused
begin
Result := FDetailsAdapter;
end;
end.
unit Details.Adapter;
uses
Details, Winapi.Windows, SysUtils;
type
TDetailsAdapter = class(TInterfacedObject, IDetailsAdapter)
private
FID: Integer;
public
procedure SetID(AID: Integer);
function GetID: Integer;
published
property ID: Integer read GetID write SetID;
end;
implementation
procedure TDetailsAdapter.SetID(AID: Integer);
begin
FID := AID;
OutputDebugString(PWideChar('OrderDetail ID set to ' + IntToStr(FID)));
end;
function TDetailsAdapter.GetID: Integer;
begin
Result := FID;
end;
end.
unit Order.Adapter;
uses
Order, Order.View, Details, Factory,
Vcl.Forms;
type
TOrderAdapter = class(TInterfacedObject, IOrderAdapter)
private
[inject]
FDetailsAdapter: IDetailsAdapter;
public
constructor Create;
procedure ButtonClick(Sender: TObject);
end;
var
OrderForm: TOrderForm;
implementation
constructor TOrderAdapter.Create;
begin
OrderForm.Button1.OnClick := ButtonClick;
end;
procedure TOrderAdapter.ButtonClick(Sender: TObject);
begin
FDetailsAdapter.ID := 5;
end;
end.
The container uses RTTI to collect the members that have this attribute and injects the correct services into them.
I have a list of records. Each record has an
URL:= string
field. Via GUI the user can edit revords or even delete records (rows) entirely. I would like to download in background in a thread all online files pointed by the URL field. Of course, I don't want to lock the GUI when the thread downloads the files. So, how do I make sure the program/user cannot access the record processed currently by the thread?
I really like to use BITS for downloads.
Access from Delphi is easy. In BITS your define jobs, which are downloaded in background. When ready you can call a EXE, you can poll in the idle loop for the result or you can get an event.
Here is a samples - you will need the jedi lib!
That sample needs to be extended for production quality (error handling, logging, job name)!
unit uc_DownloadBits;
interface
uses
ExtActns;
type
TDownloadBits = class
public
class procedure DownloadForground(ziel, downloadurl: WideString; DownloadFeedback:TDownloadProgressEvent);
class procedure DownloadBackground(ziel, downloadurl, ExeName, Params: WideString);
class procedure CompleteJob(JobId: WideString);
end;
implementation
uses
ComObj, ActiveX, SysUtils,
JwaBits, JwaBits1_5, Windows;
{ TDownloadBits }
class procedure TDownloadBits.CompleteJob(JobId: WideString);
var
bi: IBackgroundCopyManager;
job: IBackgroundCopyJob;
g: TGuid;
begin
bi:=CreateComObject(CLSID_BackgroundCopyManager) as IBackgroundCopyManager;
g:=StringToGUID(jobid);
bi.GetJob(g,job);
job.Complete();
end;
class procedure TDownloadBits.DownloadBackground(ziel, downloadurl,
ExeName, Params: WideString);
var
bi: IBackgroundCopyManager;
job: IBackgroundCopyJob;
job2: IBackgroundCopyJob2;
jobId: TGUID;
r: HRESULT;
begin
bi:=CreateComObject(CLSID_BackgroundCopyManager) as IBackgroundCopyManager;
r:=bi.CreateJob('Updatedownload', BG_JOB_TYPE_DOWNLOAD, JobId, job);
if not Succeeded(r) then
raise Exception.Create('Create Job Failed');
r:=Job.AddFile(PWideChar(downloadurl), PWideChar(ziel));
if not Succeeded(r) then
raise Exception.Create('Add File Failed');
// Download starten
Job.Resume();
Params:=Params+' '+GUIDToString(jobId);
Job2 := Job as IBackgroundCopyJob2;
Job2.SetNotifyCmdLine(pWideChar(ExeName), PWideChar(Params));
Job.SetNotifyFlags(BG_NOTIFY_JOB_TRANSFERRED);
end;
class procedure TDownloadBits.DownloadForground(ziel, downloadurl: widestring; DownloadFeedback:TDownloadProgressEvent);
var
bi: IBackgroundCopyManager;
job: IBackgroundCopyJob;
jobId: TGUID;
r: HRESULT;
// Status Zeug
p: BG_JOB_PROGRESS;
s: BG_JOB_STATE;
// Timer Zeug
hTimer: THandle;
DueTime: TLargeInteger;
c: boolean;
begin
bi:=CreateComObject(CLSID_BackgroundCopyManager) as IBackgroundCopyManager;
r:=bi.CreateJob('Updatedownload', BG_JOB_TYPE_DOWNLOAD, JobId, job);
if not Succeeded(r) then
raise Exception.Create('Create Job Failed');
r:=Job.AddFile(PWideChar(downloadurl), PWideChar(ziel));
if not Succeeded(r) then
raise Exception.Create('Add File Failed');
// Download starten
Job.Resume();
DueTime:=-10000000;
hTimer:=CreateWaitableTimer(nil, false, 'EinTimer');
SetWaitableTimer(hTimer, DueTime, 1000, nil, nil, false);
while True do
begin
Job.GetState(s);
if s in [BG_JOB_STATE_TRANSFERRING, BG_JOB_STATE_TRANSFERRED] then
begin
Job.GetProgress(p);
DownloadFeedback(nil, p.BytesTransferred, p.BytesTotal, dsDownloadingData, '', c);
if c then
break;
end;
if s in [BG_JOB_STATE_TRANSFERRED,
BG_JOB_STATE_ERROR,
BG_JOB_STATE_TRANSIENT_ERROR] then
break;
WaitForSingleObject(hTimer, INFINITE);
end;
CancelWaitableTimer(hTimer);
CloseHandle(hTimer);
if s=BG_JOB_STATE_TRANSFERRED then
job.Complete();
job:=nil;
bi:=nil;
end;
end.
So, how do I make sure the program/user cannot access the record
processed currently by the thread?
In "modern" (sine Delphi 2006 I think) records you can use properties with getters and setters just as with classes. In the setter you can prevent or allow changes to the underlying field.
A naive example:
type
TMyRecord = record
private
FURL: string;
FDownloading: boolean;
procedure SetTheURL(NewURL: string);
public
property TheURL: string read FURL write SetTheURL;
procedure DownLoad;
end;
procedure TMyRecord.SetTheURL(NewURL: string);
begin
if not FDownloading then
FURL := NewURL;
else
// signal inability to change
end;
procedure TMyRecord.DownLoad;
begin
FDownLoading := True;
// hand the downloading task to a thread
end;
Here's the documentation under Records(advanced)
Following is based on the solution of Tom Brunberg using a record. Idea that the record will begin downloading via a TThread (the implementation of the download itself is out of the question as I understand). It might be a bit rough, let me know if there are severe mistakes for example in handling the thread.
While downloading, the data is not accessible, I decided to throw an exception when accessed, but that's up to implementation details of the GUI. property IsDownLoading: Boolean can be used to e.g. disable controls that would normally make the data accessible, too.
Still, the URL can be changed at all times by user, terminating the current download if in process.
A TDownloadThread should only be present while needed. If there are lots of these records, this should reduce unneeded resources.
unit Unit1;
interface
uses
System.Classes, System.SysUtils;
type
TDownLoadThread = class(TThread)
private
FURL: string;
FData: Variant;
procedure SetURL(const Value: string);
protected
procedure Execute; override;
public
property Data: Variant read FData;
property URL: string read FURL write SetURL;
end;
TDownLoadRecord = record
private
FData: Variant;
FURL: string;
FDownLoadThread: TDownLoadThread;
procedure DownLoadThreadTerminate(Sender: TObject);
function GetIsDownLoading: Boolean;
procedure SetURL(const Value: string);
procedure URLChanged;
function GetData: Variant;
public
property Data: Variant read GetData;
property URL: string read FURL write SetURL;
property IsDownLoading: Boolean read GetIsDownLoading;
end;
implementation
{ TDownLoadRecord }
procedure TDownLoadRecord.DownLoadThreadTerminate(Sender: TObject);
begin
FData := FDownLoadThread.Data;
FDownLoadThread := nil;
end;
function TDownLoadRecord.GetData: Variant;
begin
if not IsDownLoading then
Result := FData
else
raise Exception.Create('Still downloading');
end;
function TDownLoadRecord.GetIsDownLoading: Boolean;
begin
Result := (FDownLoadThread <> nil) and not FDownLoadThread.Finished;
end;
procedure TDownLoadRecord.SetURL(const Value: string);
begin
if FURL <> Value then
begin
FURL := Value;
URLChanged;
end;
end;
procedure TDownLoadRecord.URLChanged;
begin
if FURL <> '' then
begin
if FDownLoadThread <> nil then
TDownLoadThread.Create(True)
else
if not FDownLoadThread.CheckTerminated then
FDownLoadThread.Terminate;
FDownLoadThread.URL := FURL;
FDownLoadThread.FreeOnTerminate := True;
FDownLoadThread.OnTerminate := DownLoadThreadTerminate;
FDownLoadThread.Start;
end;
end;
{ TDownLoadThread }
procedure TDownLoadThread.Execute;
begin
// Download
end;
procedure TDownLoadThread.SetURL(const Value: string);
begin
FURL := Value;
end;
end.
I'm trying to use TVirtualInterface. I've mostly tried to follow the examples at the Embarcadero doc wiki and at Nick Hodges' blog.
However, What I'm trying to do is a little bit different from the standard examples.
I have simplified the following sample code as much as I can to illustrate what I am trying to do. I have left out obvious validation and error handling code.
program VirtualInterfaceTest;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.Generics.Collections,
System.Rtti,
System.SysUtils,
System.TypInfo;
type
ITestData = interface(IInvokable)
['{6042BB6F-F30C-4C07-8D3B-C123CF1FF60F}']
function GetComment: string;
procedure SetComment(const Value: string);
property Comment: string read GetComment write SetComment;
end;
IMoreData = interface(IInvokable)
['{1D2262CE-09F4-45EC-ACD8-3EEE6B2F1548}']
function GetSuccess: Boolean;
procedure SetSuccess(const Value: Boolean);
property Success: Boolean read GetSuccess write SetSuccess;
end;
TDataHolder = class
private
FTestData: ITestData;
FMoreData: IMoreData;
public
property TestData: ITestData read FTestData write FTestData;
property MoreData: IMoreData read FMoreData write FMoreData;
end;
TVirtualData = class(TVirtualInterface)
private
FData: TDictionary<string, TValue>;
procedure DoInvoke(Method: TRttiMethod;
const Args: TArray<TValue>;
out Result: TValue);
public
constructor Create(PIID: PTypeInfo);
destructor Destroy; override;
end;
constructor TVirtualData.Create(PIID: PTypeInfo);
begin
inherited Create(PIID, DoInvoke);
FData := TDictionary<string, TValue>.Create;
end;
destructor TVirtualData.Destroy;
begin
FData.Free;
inherited Destroy;
end;
procedure TVirtualData.DoInvoke(Method: TRttiMethod;
const Args: TArray<TValue>;
out Result: TValue);
var
key: string;
begin
if (Pos('Get', Method.Name) = 1) then
begin
key := Copy(Method.Name, 4, MaxInt);
FData.TryGetValue(key, Result);
end;
if (Pos('Set', Method.Name) = 1) then
begin
key := Copy(Method.Name, 4, MaxInt);
FData.AddOrSetValue(key, Args[1]);
end;
end;
procedure InstantiateData(obj: TObject);
var
rttiContext: TRttiContext;
rttiType: TRttiType;
rttiProperty: TRttiProperty;
propertyType: PTypeInfo;
data: IInterface;
value: TValue;
begin
rttiContext := TRttiContext.Create;
try
rttiType := rttiContext.GetType(obj.ClassType);
for rttiProperty in rttiType.GetProperties do
begin
propertyType := rttiProperty.PropertyType.Handle;
data := TVirtualData.Create(propertyType) as IInterface;
value := TValue.From<IInterface>(data);
// TValueData(value).FTypeInfo := propertyType;
rttiProperty.SetValue(obj, value); // <<==== EInvalidCast
end;
finally
rttiContext.Free;
end;
end;
procedure Test_UsingDirectInstantiation;
var
dataHolder: TDataHolder;
begin
dataHolder := TDataHolder.Create;
try
dataHolder.TestData := TVirtualData.Create(TypeInfo(ITestData)) as ITestData;
dataHolder.MoreData := TVirtualData.Create(TypeInfo(IMoreData)) as IMoreData;
dataHolder.TestData.Comment := 'Hello World!';
dataHolder.MoreData.Success := True;
Writeln('Comment: ', dataHolder.TestData.Comment);
Writeln('Success: ', dataHolder.MoreData.Success);
finally
dataHolder.Free;
end;
end;
procedure Test_UsingIndirectInstantiation;
var
dataHolder: TDataHolder;
begin
dataHolder := TDataHolder.Create;
try
InstantiateData(dataHolder); // <<====
dataHolder.TestData.Comment := 'Hello World!';
dataHolder.MoreData.Success := False;
Writeln('Comment: ', dataHolder.TestData.Comment);
Writeln('Success: ', dataHolder.MoreData.Success);
finally
dataHolder.Free;
end;
end;
begin
try
Test_UsingDirectInstantiation;
Test_UsingIndirectInstantiation;
except on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
I have some arbitrary interfaces with read/write properties, ITestData and IMoreData, and a class that holds references to these interfaces, IDataHolder.
I have created a class, TVirtualData, that inherits from TVirtualInterface, following Nick Hodges' examples. And when I use this class the way I see it in all the examples, as in Test_UsingDirectInstantiation, it works swell.
What my code needs to do, however, is instantiate the interfaces in a more indirect manner, as in Test_UsingIndirectInstantiation.
The InstantiateData method uses RTTI, and works well up until the SetValue call which throws an EInvalidCast exception ("Invalid class typecast").
I added in the commented line (which I saw in some sample code from "Delphi Sorcery") to try to cast the data object to the appropriate interface. This allowed the SetValue call to run cleanly, but when I tried to access the interface property (i.e. dataHolder.TestData.Comment) it threw a EAccessViolation exception ("Access violation at address 00000000. Read of address 00000000").
For fun I replace IInterface in the InstantiateData method with ITestData, and for the first property it worked fine, but naturally, it didn't work for the second property.
Question: Is there a way to dynamically cast this TVirtualInterface object to the appropriate interface using TypeInfo or RTTI (or something else) so that the InstantiateData method has the same effect as setting the properties directly?
First you have to cast the instance to the correct interface and not IInterface. You can still store it in an IInterface variable though but it really containes the reference to the correct interface type.
Then you have to put that into a TValue with the correct type and not IInterface (RTTI is very strict about types)
The commented line you added was just to work around the second but as it was really containing the IInterface reference (and not a ITestData or TMoreData references) it resulted on the AV.
procedure InstantiateData(obj: TObject);
var
rttiContext: TRttiContext;
rttiType: TRttiType;
rttiProperty: TRttiProperty;
propertyType: PTypeInfo;
data: IInterface;
value: TValue;
begin
rttiType := rttiContext.GetType(obj.ClassType);
for rttiProperty in rttiType.GetProperties do
begin
propertyType := rttiProperty.PropertyType.Handle;
Supports(TVirtualData.Create(propertyType), TRttiInterfaceType(rttiProperty.PropertyType).GUID, data);
TValue.Make(#data, rttiProperty.PropertyType.Handle, value);
rttiProperty.SetValue(obj, value);
end;
end;
In my library i'm invoking methods under specific conditions, which requires stdcall calling convention. Currently i'm using compiler static resolution, implemented as rather large list of well-known method signatures and corresponding overloaded versions of my subroutine. This works but looks quite fugly and doesnt 100% cover all possible methods. I would like to add a possibility to work with generic method pointer and assert proper calling convention by asking RTTI. And here i'm stuck, please advise.
Input: code/data pair of pointers as in TMethod
Output: boolean indicator, true if method is stdcall
I'd preferable use "classic" RTTI to create less version dependencies, however i cant find any calling convention indicator within "classic" RTTI...
NB: This question is UNRELATED to importing external functions
You can extract calling convention information from extended RTTI (available since Delphi 2010).
uses RTTI, TypInfo;
function GetMethCallConv(AMeth: TMethod; out Conv: TCallConv): Boolean;
var
Ctx: TRttiContext;
Meth: TRttiMethod;
Typ: TRttiType;
begin
Ctx:= TRttiContext.Create;
try
Typ:= Ctx.GetType(TObject(AMeth.Data).ClassType);
for Meth in Typ.GetMethods do begin
if Meth.CodeAddress = AMeth.Code then begin
Conv:= Meth.CallingConvention;
Exit(True);
end;
end;
Exit(False);
finally
Ctx.Free;
end;
end;
//test
type
TMyObj = class
public
procedure MyMeth(I: Integer); stdcall;
end;
procedure TMyObj.MyMeth(I: Integer);
begin
ShowMessage(IntToStr(I));
end;
procedure TForm2.Button2Click(Sender: TObject);
var
Conv: TCallConv;
Meth: TMethod;
MyObj: TMyObj;
begin
MyObj:= TMyObj.Create;
Meth.Code:= #TMyObj.MyMeth;
Meth.Data:= MyObj;
if GetMethCallConv(Meth, Conv) then begin
case Conv of
ccReg: ShowMessage('Register');
ccCdecl: ShowMessage('cdecl');
ccPascal: ShowMessage('Pascal');
ccStdCall: ShowMessage('StdCall');
ccSafeCall: ShowMessage('SafeCall');
end;
end;
MyObj.Free;
end;
Update
For "classic" RTTI read Sertac answer; the following works OK on Delphi 2010:
uses ObjAuto;
function GetMethCallConv2(AMeth: TMethod; out Conv: TCallingConvention): Boolean;
var
Methods: TMethodInfoArray;
I: Integer;
P: PMethodInfoHeader;
begin
Result:= False;
Methods:= GetMethods(TObject(AMeth.Data).ClassType);
if not Assigned(Methods) then Exit;
for I:= Low(Methods) to High(Methods) do begin
P:= Methods[I];
if P^.Addr = AMeth.Code then begin
Inc(Integer(P), SizeOf(TMethodInfoHeader) - SizeOf(ShortString) + 1 +
Length(PMethodInfoHeader(P)^.Name));
Conv:= PReturnInfo(P).CallingConvention;
Result:= True;
Exit;
end;
end;
end;
{$TYPEINFO ON}
{$METHODINFO ON}
type
TMyObj = class
public
procedure MyMeth(I: Integer);
end;
procedure TMyObj.MyMeth(I: Integer);
begin
ShowMessage(IntToStr(I));
end;
procedure TForm2.Button3Click(Sender: TObject);
var
Conv: TCallingConvention;
Meth: TMethod;
MyObj: TMyObj;
begin
MyObj:= TMyObj.Create;
Meth.Code:= #TMyObj.MyMeth;
Meth.Data:= MyObj;
if GetMethCallConv2(Meth, Conv) then begin
case Conv of
ccRegister: ShowMessage('Register');
ccCdecl: ShowMessage('cdecl');
ccPascal: ShowMessage('Pascal');
ccStdCall: ShowMessage('StdCall');
ccSafeCall: ShowMessage('SafeCall');
end;
end;
MyObj.Free;
end;
Including Delphi 7 and up, when METHODINFO directive is on, run-time generates information about, at least having public visibility, method parameters and return types and calling convention (TYPEINFO should also be on).
Not sure if the below sample would help you directly since it works on an instance and method's name and not its address, but perhaps you can construct a look-up table for name-address of methods beforehand.
type
{$METHODINFO ON}
TSomeClass = class
public
procedure Proc1(i: Integer; d: Double); stdcall;
procedure Proc2;
end;
{$METHODINFO OFF}
TForm1 = class(TForm)
Button1: TButton;
procedure FormCreate(Sender: TObject);
private
FSomeClass: TSomeClass;
..
uses
objauto;
procedure TForm1.FormCreate(Sender: TObject);
begin
FSomeClass := TSomeClass.Create;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Info: Pointer;
begin
Info := GetMethodInfo(FSomeClass, 'Proc1');
if Assigned(Info) then begin
Inc(Integer(Info), SizeOf(TMethodInfoHeader) - SizeOf(ShortString) + 1 +
Length(PMethodInfoHeader(Info).Name));
if PReturnInfo(Info).CallingConvention = ccStdCall then
// ...
end;
Beware and do some testing though, tested on D2007 the working is somewhat unpredictable. For instance, if the above 'Proc1' is changed to procedure Proc1(i: Pointer; d: Double); no detailed RTTI is generated.
See here on how to find out:
http://rvelthuis.de/articles/articles-convert.html#cconvs
IOW, you can simply try if it works, or you take a look at the exported name (_name#17 or similar) or you take a look at a disassembly, e.g. in the CPU view.