分享

Dynamic Method Invocation in Delphi

 quasiceo 2012-12-08

Dynamic Method Invocation in Delphi (Part 1)

As Danny Thorpe pointed out recently, Delphi has had dynamic method invocation for many years before C#. First post of a long story...

A few weeks ago (I've collected many links in my "for blog" bookmarks recently) Danny Thorpe blogged about Dynamic Method Invocation, claiming Delphi did years ago what C# is aiming at now (the history of Delphi leading on the tech side is repeating). His post was a response to the post of yet-another ex-Borlander, Charlie Calvert, who posted about Dynamic Lookups in C# in a Future Focus column of his blog. I find the proposed C# syntax confusing, and don't think that making out the section of code for dynamic invocation at the caller side makes a lot of sense, but I had only a cursory view.


Let me start (today) by showing you how the call looks like:

var
  v: variant;
begin
  v := VarDSLDateTimeCreate (now);
  v.NextMonday;
  v.am7;

This sets the date and time represented by the variant to 7 am next Monday. I had a much better invocation mechanism by embedding the variant within an actual object, something I'll work on for a future post. Before that, I'll show you the custom variant architecture. All I want to show you for today, is that NextMonday and 7am are not hard-coded functions. They don't exist. The object receives a call and parses and interprets the function name to determine what to do! Here is a snippet, I'll expand soon with the details:

function TDslDateTimeVariantType.DoProcedure(const V: TVarData;
  const Name: string; const Arguments: TVarDataArray): Boolean;
var
  tmp: string;
  value, month, day: Integer;
  tmpDate: TDateTime;
begin
  if Pos ('AM', Name) > 0 then
  begin
    // parse and process...
    tmp := StringReplace (Name, 'AM', '', []);
    value := StrToIntDef (tmp, 0);

// yes this is ugly!!!
    TDslDateTimeVarData(V).VDateTime.DateTime :=
      RecodeTime (TDslDateTimeVarData(V).VDateTime.DateTime, value, 0, 0, 0);
  end;

...

  if Pos ('NEXT', Name) > 0 then
  begin
    // parse and process...
    tmp := StringReplace (Name, 'NEXT', '', []);
    if tmp = 'MONDAY' then
    begin
      // compute next monday date...
...

Stay tuned for the rest of the code and an improved version of it.


Dynamic Method Invocation in Delphi (Part 2)

I should have posted the rationale behind dynamic method invocation before getting to the tech details... but at least I'm doing it before delving into the code.

My post yesterday on dynamic methods invocation in Delphi has raised a few doubts about the rationale behind sch an "unnatural" way of writing Delphi code. I should have posted the rationale behind dynamic method invocation before starting showing code... but at least I'm doing it before delving into the actual tech details, a post I originally wanted to do today, but I'm delaying.

So do you want to invoke a method that doesn't exists at compile-time? There isn't a single answer, but I have used similar coding in real projects and have seen trends (mostly in other languages) going in this direction. So here is a short list of reasons:

  • It is trendy. I know, this is not a technical reason. But with people who keep complaining Delphi is always behind every other language, I think it is worth pointing out Delphi has features that other popular languages (Java, C#) are thinking of adding now. And why are they adding this features? Mostly because they are trendy, and they have become trendy because Ruby on Rails is heavily based on them. Again, does it make any sense for Delphi to clone Ruby features? Certainly not, but claiming "hey we can do it as well" is certainly legitimate.
  • Dynamic invocation is a common features of dynamic languages, from Ruby to JavaScript, which are not compiled in a strict sense. As Delphi developers you probably share with me some aversion for slow, late-bound, non compile-checked language features. I won't certainly use them as a standard coding practice. However there are situations in which we end up doing the same, in an even worse way. have you even accessed a property by name with RTTI? Have you ever passed a database field name (the string, I mean) as parameter? Every time you use a string to "parameterize" the meaning of a function call you are delaying the resolution of a symbol at runtime. With dynamic invocation you are doing exactly the same, only you are using the language base syntax rather than a convoluted string based one. How comes is setting a dynamic property with a variant is much worse than calling SetPropValue? The former can be more readable.
  • Although I'm a string supporter of strongly typing and related features, I appreciate a lot when flexibility is build into a program in a way I don't have to recompile it when I could only reconfigure it. From DFM files to configuration files, to applications that change depending on database metadata, there are many cases in which you want flexibility into your programs.
  • I've used this technique while toying with Domain Specific Languages. An internal DSL is like a sub-language hosted by your language and written according to its syntax, but meant almost for non programmers, easy to read and understand, not too bound to the language syntax. This is the situation in which calling date.am7 could be better that data.at (7, 'am'), the former is easier to write and read, especially for a non-programmer. You might not like the entire DSL movement, but I find it quite interesting.
  • Coding against partially undefined (at compile time) or flexible data structures is quite neat. Won't it be nice to say table1.usename when the latter is a field of the table rather than writing table1.fieldbyname('username')? Sure, Delphi can generate the field definitions for you with the fields editor, but when you hit a table with a slightly different structure you are stuck. I ended using a similar solution to the one outlined here for XML DOM navigation. In my code (well, I wrote it with others) you could write data.user.name to get the name node under the user node of the document. Of course, the document needed to have those specific nodes or you'd have a runtime error. But the XML document could have different formats provided those nodes where present, something you don't get if you import the data structure at compile time and let Delphi define interfaces for you.

I might not have convinced you, I know, but here are some reasons I find interesting for using dynamic method invocation when it is needed. I will never give up strong type-checking as a general compiler rule, but I appreciate exceptions. The trouble, though, is that writing this code in Delphi is much more difficult than I'd like... but that something for another post (which might take a few day, due to Easter holidays. For now, if you have other reasons for using (or not using) dynamic methods invocation feel free to comment here (or in yuor blog, posting a link here, as you prefer).

To summarize, Delphi is more powerful and flexible than most of its user think. Let's use that power when needed...


Dynamic Method Invocation in Delphi (Part 3)

The actual source code of the custom variant used to implement dynamic method invocation.

After two introductory posts, here is the actual source code of the custom variant used to implement dynamic method invocation. It is only a rough working prototype. Time permitting, I'd integrate it into the actual class hosting the data (something I did in the past for a different demo). And will try to make the code easier to reuse. Anyway, here is goes. This rather complex code allows you to call method that don't actually exist, as you'll pass the method name as a string parameter to a single processing function. The same could be done for functions and properties. Enjoy.

      
    unit DynamicDslVariant; interface uses TypInfo, Variants, Classes; function VarDSLDateTimeCreate(const AValue: TDateTime): Variant; overload; implementation uses VarUtils, SysUtils, DateUtils, WIndows, Dialogs; type TDslDateTimeVariantType = class (TPublishableVariantType) protected function GetInstance(const V: TVarData): TObject; override; public procedure Clear(var V: TVarData); override; procedure CastTo(var Dest: TVarData; const Source: TVarData; const AVarType: TVarType); override; function DoFunction(var Dest: TVarData; const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; override; function DoProcedure(const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; override; end; var DslDateTimeVariantType: TDslDateTimeVariantType = nil; type TDslDateTimeData = class (TPersistent) private FDateTime: TDateTime; public constructor Create(const AValue: TDateTime); overload; function AsString: string; property DateTime: TDateTime read FDateTime write FDateTime; end; { Helper record for variant data } TDslDateTimeVarData = packed record VType: TVarType; Reserved1, Reserved2, Reserved3: Word; VDateTime: TDslDateTimeData; Reserved4: DWord; end; { TDslDateTimeVariantType } procedure TDslDateTimeVariantType.CastTo(var Dest: TVarData; const Source: TVarData; const AVarType: TVarType); var LTemp: TVarData; begin if Source.VType = VarType then case AVarType of varString: VarDataFromStr(Dest, TDslDateTimeVarData (Source).VDateTime.AsString); else VarDataInit(LTemp); try LTemp.VType := varDate; LTemp.VDate := TDslDateTimeVarData (Source).VDateTime.DateTime; VarDataCastTo(Dest, LTemp, AVarType); finally VarDataClear(LTemp); end; end else inherited; end; procedure TDslDateTimeVariantType.Clear(var V: TVarData); begin V.VType := varEmpty; FreeAndNil(TDslDateTimeVarData(V).VDateTime); end; function TDslDateTimeVariantType.DoFunction(var Dest: TVarData; const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; begin ShowMessage ('fn calling ' + name); // Dest := Result := True; end; function TDslDateTimeVariantType.DoProcedure(const V: TVarData; const Name: string; const Arguments: TVarDataArray): Boolean; var tmp: string; value, month, day: Integer; tmpDate: TDateTime;begin // ShowMessage ('proc calling ' + name); if Pos ('AM', Name) > 0 then begin // parse and process... tmp := StringReplace (Name, 'AM', '', []); value := StrToIntDef (tmp, 0); TDslDateTimeVarData(V).VDateTime.DateTime := RecodeTime (TDslDateTimeVarData(V).VDateTime.DateTime, value, 0, 0, 0); end; if Pos ('PM', Name) > 0 then begin // parse and process... tmp := StringReplace (Name, 'PM', '', []); value := StrToIntDef (tmp, 0); TDslDateTimeVarData(V).VDateTime.DateTime := RecodeTime (TDslDateTimeVarData(V).VDateTime.DateTime, value + 12, 0, 0, 0); end; if Pos ('NEXT', Name) > 0 then begin // parse and process... tmp := StringReplace (Name, 'NEXT', '', []); if tmp = 'MONDAY' then begin tmpDate := Now; value := DayOfWeek(tmpDate); tmpDate := tmpDate + 7 - value + 2; // monday as 2nd day of the week TDslDateTimeVarData(V).VDateTime.DateTime := DateOf (tmpDate) + TimeOf (TDslDateTimeVarData(V).VDateTime.DateTime); end; end; if Pos ('DEC', Name) > 0 then begin month := 12; // parse and process... tmp := StringReplace (Name, 'DEC', '', []); day := StrToIntDef (tmp, 0); TDslDateTimeVarData(V).VDateTime.DateTime := RecodeDate (TDslDateTimeVarData(V).VDateTime.DateTime, YearOf (now), month, day); end; Result := True; end; function TDslDateTimeVariantType.GetInstance(const V: TVarData): TObject; begin Result := TDslDateTimeVarData(V).VDateTime; end; { TDslDateTimeData } function TDslDateTimeData.AsString: string; begin Result := DateTimeToStr (FDateTime); end; constructor TDslDateTimeData.Create(const AValue: TDateTime); begin FDateTime := AValue; end; // variant construction function VarDSLDateTimeCreate(const AValue: TDateTime): Variant; begin VarClear(Result); TDslDateTimeVarData(Result).VType := DslDateTimeVariantType.VarType; TDslDateTimeVarData(Result).VDateTime := TDslDateTimeData.Create(AValue); end; initialization DslDateTimeVariantType := TDslDateTimeVariantType.Create; finalization FreeAndNil(DslDateTimeVariantType); end.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多