Archive
N++ context parser, part 2
Right. In my last article on parsing I introduced a basic buffer class and skeleton parser. In this article I introduce a working 2 level parser, meaning recursive, which implements both the program() and execute() code blocks. Being recursive means that you can now nest several execute() sections inside each other. But at this stage that’s pretty much all I’ve had time to implement — so consider it a rough sketch.
Support
At the moment we have a program parser, which naturally parses a program block which looks like this:
program("name") { }
Registered for the program parser, is a single sub-parser for the “execute” keyword, this is the class TQTXExecuteParser class. This allows you to have execute() sections within a program block, as such:
program("name") { execute (scope) { /* Code goes heres */ } }
To avoid recursive stack errors (stack overflow), the execute class does a trick. Namely that it registers support for itself (execute keyword) at parse-time, meaning that an execute block will support execute child blocks, like this:
program("name") { execute (scope) { /* Sub execute block */ execute (scope) { /* your code here */ } } }
Well, basically this is the base system in place. Adding new keywords and features is now a matter of creating sub parser classes and registering them to either the execute class or the program parser class. The benefit of such a simple system is that it’s fast, easy to understand and use – and easy to maintain. Concrete rules for parsing can be defined, and a lot of code re-cycled between the base-class (TQTXCustomParser) and it’s decendants.
With a bit of work you will have a fine-tuned, fast and efficient language parser.
Supporting other languages
As you probably guessed it’s not that hard to add support for other languages. I mean once you take the time to implement the basic form of a language unit or program-file. But it’s always a time consuming task, where even the tiniest detail matters. If you miss a single language rule (like say, not skippig a comma after it’s been consumed) the error will spread into the next parser and so on – until an exception occurs.
But, once you get the hang of chewing through a source-buffer, supporting enough parsers and keywords (and for Delphi also states, like the “Inerface” keyword state and the “implementation” keyword state) you can pretty much mix and match all you want.
Well, without going to far into the code — here is the updated source. You may want to drop memo control on your form and add the following code:
procedure TForm1.W3Button1Click(Sender: TObject); var mContext: TQTXContext; mParser: TQTXLPPParser; begin mContext:=TQTXContext.Create; try mParser:=TQTXLPPParser.Create(mContext); try mContext.Buffer.LoadFromString(w3memo1.text); try mParser.Parse; except on e: exception do writeln(e.message); end; finally mParser.free; end; finally mContext.free; end; end;
And set the content of the memo to:
procedure TForm1.InitializeForm; begin inherited; w3memo1.Text:=#'program("test") { execute (*) { /* Code here */ } }'; end;
Ok — here is the updated parser code:
unit qtxparser; interface uses SmartCL.System; const TOK_ROUNDOPEN = 1; // "(" TOK_ROUNDCLOSE = 2; // ")" TOK_CUROPEN = 3; // "{" TOK_CURCLOSE = 4; // "}" TOK_SPACE = 5; // " " TOK_ADD = 6; // "+" TOK_SUBTRACT = 7; // "-" TOK_DIVIDE = 8; // "/" TOK_POWER = 9; // "^" TOK_COMMA =10; // "," TOK_COLON =11; // ";" Numeric_Operators: Array [0..3] of Integer = (TOK_ADD,TOK_SUBTRACT,TOK_DIVIDE,TOK_POWER); type ENPPException = Class(EW3Exception); TLPPApplicationObject = Class; TQTXBuffer = Class(TObject) private FData: String; FIndex: Integer; FLineNr: Integer; protected function getCurrent:String;virtual; public Property LineNr:Integer read FLineNr; Property Current:String read getCurrent; Function Back:Boolean; function Next:Boolean; function BOF:Boolean; function EOF:Boolean; procedure SkipJunk; function ReadTo(Const aChars:array of string; var text:String):Boolean;overload; function ReadTo(aValue:String):Boolean;overload; function ReadTo(aValue:String;var inner:String):Boolean;overload; function ReadWord(var Text:String):Boolean; function ReadToEOL(var text:String):Boolean; function PeekAhead(aCount:Integer;var Text:String):Boolean; function Compare(aText:String):Boolean; Property TextFromCurrent:String read ( Copy(FData,FIndex,length(FData)-FIndex) ); procedure First; procedure LoadFromString(Text:String); procedure Clear; End; TQTXContext = Class(TObject) Private FBuffer: TQTXBuffer; public Property Buffer:TQTXBuffer read FBuffer; Property ApplicationObject:TLPPApplicationObject; Constructor Create;virtual; Destructor Destroy;Override; end; TQTXCustomParser = Class; TQTXCustomParserClass = Class of TQTXCustomParser; TQTXParserInfo = Class(TObject) public property Instance: TQTXCustomParser; Property Keyword:String; end; TQTXCustomParser = Class(TObject) private FContext: TQTXContext; FParsers: Array of TQTXParserInfo; protected function getParserIndexFor(keyword:String; var aIndex:Integer):Boolean; function getParserInstanceFor(keyword:String; var aParser:TQTXCustomParser):Boolean; procedure RegisterParser(keyword:String; const aParser:TQTXCustomParser); protected procedure ParseAsExecuteBlock;virtual; protected Procedure ClearParserInstances;virtual; public Property Context:TQTXContext read FContext; procedure Parse;virtual; Constructor Create(aContext:TQTXContext);virtual; Destructor Destroy;Override; end; (* Parser for the keyword "program" *) TQTXProgramParser = Class(TQTXCustomParser) protected procedure ParseProgramName; procedure Parse;override; public Constructor Create(aContext:TQTXContext);override; end; TQTXExecuteParser = class(TQTXCustomParser) protected procedure ParseScope; procedure Parse;override; public Constructor Create(aContext:TQTXContext);override; end; TQTXLPPParser = Class(TQTXCustomParser) protected procedure Parse;override; public Constructor Create(aContext:TQTXContext);override; end; //########################################################################## // In-memory program model classes //########################################################################## TLPPObject = Class(Tobject) end; TLPPModule = Class(TLPPObject) public Property Name:String; end; TLPPModuleList = Array of TLPPModule; TLPPApplicationObject = Class(TLPPObject) private FModules: TLPPModuleList; public Property ApplicationName:String; Property Modules:TLPPModuleList read FModules; function IndexOfModule(aModuleName:String):Integer; Procedure Clear; Constructor Create;virtual; Destructor Destroy;Override; end; implementation var TOK_SYM: Array[TOK_ROUNDOPEN..TOK_COLON] of string = ('(',')','{','}',' ','+','-','/','^',',',';'); //########################################################################### // TLPPApplicationObject //########################################################################### Constructor TLPPApplicationObject.Create; begin inherited Create; end; Destructor TLPPApplicationObject.Destroy; Begin clear; inherited Destroy; end; function TLPPApplicationObject.IndexOfModule(aModuleName:String):Integer; var x: Integer; Begin result:=-1; aModulename:=trim(lowercase(aModuleName)); if length(aModuleName)>0 then begin for x:=0 to FModules.Count-1 do Begin if lowercase(FModules[x].Name)=aModuleName then begin result:=x; break; end; end; end; end; Procedure TLPPApplicationObject.Clear; begin ApplicationName:=''; while FModules.Count>0 do begin FModules[0].free; FModules[0]:=NIL; FModules.Delete(0,1); end; end; //########################################################################### // TQTXLPPParser //########################################################################### Constructor TQTXLPPParser.Create(aContext:TQTXContext); begin inherited Create(aContext); (* Register the "program" keyword-parser *) RegisterParser('program',TQTXProgramParser.Create(context)); end; procedure TQTXLPPParser.Parse; var mCache: String; mSubParser: TQTXCustomParser; Begin repeat case Context.Buffer.Current of ' ': Begin self.Context.Buffer.SkipJunk; end; '/': Begin if context.buffer.Compare('//') then begin self.context.buffer.SkipJunk; end; end; '(': Begin if Context.Buffer.Compare('(*') then Context.buffer.SkipJunk else Begin if getParserInstanceFor(mCache,mSubParser) then begin (* Invoke sub parser *) mSubParser.Parse; (* SKip trailing junk if any *) Context.Buffer.SkipJunk; (* clear cache *) mCache:=''; end else raise ENPPException.CreateFmt ('Syntax error [%s]',[mCache]); end; end; else Begin mCache += Context.Buffer.Current; end; end; if not Context.buffer.next then break; until Context.buffer.EOF; end; //########################################################################### // TQTXExecuteParser //########################################################################### Constructor TQTXExecuteParser.Create(aContext:TQTXContext); Begin inherited Create(AContext); end; procedure TQTXExecuteParser.Parse; var mDummy: Integer; Begin if not self.getParserIndexFor('execute',mDummy) then Begin self.RegisterParser('execute',TQTXExecuteParser.Create(Context)); end; (* Expects: Scope param list (), with a bare minumum of (STAR) *) //writeln(context.buffer.TextFromCurrent); writeln('Enters execute parser'); context.buffer.SkipJunk; (* Check for "(" *) if Context.Buffer.Current=TOK_SYM[TOK_ROUNDOPEN] then Begin // Skip the "(" char Context.Buffer.Next; ParseScope; end; if context.buffer.ReadTo('{') then Begin Context.buffer.next; (* Parse content as an execute block *) self.ParseAsExecuteBlock; end else Raise Exception.Create('Syntax error, expected code-block entry error'); writeln('leaves execute parser'); end; procedure TQTXExecuteParser.ParseScope; var mScope: String; Begin (* Keep reading until we find ")", or break on others *) if context.buffer.ReadTo([')',';','{','}'],mScope) then Begin (* Valid stop? or one of the breakers? *) if context.buffer.Current=')' then Begin writeln('Defined scope for this execute block is:' + mScope); //skip ")" char context.buffer.next; end else Raise Exception.Create('Syntax error, expected scope terminator error'); end; end; //########################################################################### // TQTXProgramParser //########################################################################### Constructor TQTXProgramParser.Create(aContext:TQTXContext); begin inherited Create(aContext); RegisterParser('execute',TQTXExecuteParser.Create(context)); end; procedure TQTXProgramParser.ParseProgramName; var mTemp: String; Begin (* Expects: ("NAME") As in Name of program *) mtemp:=''; if Context.Buffer.Current='(' then begin Context.buffer.next; if Context.buffer.current='"' then begin Context.Buffer.next; if Context.buffer.readTo('"',mTemp) then begin Context.ApplicationObject.ApplicationName:=mTemp; Context.Buffer.next; Context.Buffer.SkipJunk; if Context.buffer.Current=TOK_SYM[TOK_ROUNDCLOSE] then Begin Context.Buffer.next; end else Raise Exception.Create('Syntax error, expected ")" param error'); end else Raise Exception.Create('Syntax error, expected string termination error'); //showmessage(mName); end else Raise Exception.Create('Syntax error, expected string identifier'); end else Raise Exception.Create('Syntax error, expected parameter'); end; procedure TQTXProgramParser.Parse; Begin writeln(ClassName); context.buffer.SkipJunk; (* Check for "(" *) if Context.Buffer.Current=TOK_SYM[TOK_ROUNDOPEN] then Begin ParseProgramName; writeln('Program name:' + Context.ApplicationObject.ApplicationName); context.Buffer.SkipJunk; if context.Buffer.Current=TOK_SYM[TOK_CUROPEN] then Begin context.buffer.next; repeat ParseAsExecuteBlock; Context.buffer.SkipJunk; if context.buffer.Current=TOK_SYM[TOK_CURCLOSE] then break; until Context.Buffer.EOF; end else raise exception.create('Syntax error, expected program-block entry'); end; writeln("Exiting TQTXProgramParer"); end; //########################################################################### // TQTXCustomParser //########################################################################### Constructor TQTXCustomParser.Create(aContext:TQTXContext); Begin inherited Create; FContext:=aContext; end; Procedure TQTXCustomParser.ClearParserInstances; Begin (* Release sub-parser instances if any *) while FParsers.Count>0 do Begin if FParsers[0].Instance<>NIL then Begin FParsers[0].Instance.free; FParsers[0].Instance:=NIL; end; FParsers[0].free; FParsers.Delete(0,1); end; end; Destructor TQTXCustomParser.destroy; begin ClearParserInstances; inherited; end; function TQTXCustomParser.getParserIndexFor(keyword:String; var aIndex:Integer):Boolean; var x: Integer; begin result:=False; aIndex:=-1; keyword:=lowercase(trim(keyword)); if keyword.length>0 then Begin //if FParsers.Count>0 then //begin for x:=0 to FParsers.count-1 do Begin if sameText(FParsers[x].Keyword,keyword) then begin result:=true; aIndex:=x; break; end; end; //end; end; end; function TQTXCustomParser.getParserInstanceFor(keyword:String; var aParser:TQTXCustomParser):Boolean; var x: Integer; begin result:=False; aParser:=NIL; keyword:=lowercase(trim(keyword)); if keyword.length>0 then Begin //if FParsers.Count>0 then //begin for x:=0 to FParsers.count-1 do Begin if sameText(FParsers[x].Keyword,keyword) then begin result:=true; aParser:=FParsers[x].Instance; break; end; end; //end; end; end; procedure TQTXCustomParser.RegisterParser(keyword:String; const aParser:TQTXCustomParser); var mIndex: Integer; mInfo: TQTXParserInfo; Begin keyword:=lowercase(trim(keyword)); if keyword.length>0 then Begin if aParser<>NIL then begin if not getParserIndexFor(keyword,mIndex) then Begin (* register sub parser *) mInfo:=TQTXParserInfo.Create; mInfo.Instance:=aParser; mInfo.Keyword:=keyword; FParsers.Add(mInfo); end else raise ENPPException.createFmt ('Parser for keyword [%s] already registered',[keyword]); end else raise ENPPException.CreateFmt ('Parser class for keyword [%s] was NIL error',[keyword]); end; end; procedure TQTXCustomParser.Parse; begin raise ENPPException.CreateFmt ('No parser implemented for class %s',[classname]); end; (* About: This method continues the parsing process as if it's parsing from within an execute() code block. This means that all the rules of normal 'Execute( * ) " applies. After Program() this is the default parsing mode, which makes it the fallback of more or less all parse-objects which allows ordinary code (e.g "case/switch" sections and more) *) procedure TQTXCustomParser.ParseAsExecuteBlock; var mCache: String; mTemp: String; mParser: TQTXCustomParser; Begin while not Context.Buffer.EOF do Begin case Context.Buffer.Current of ' ', #9: begin //Context.buffer.SkipJunk; end; '(': If context.Buffer.Compare('(*') and not Context.buffer.compare('(*)') then Begin Context.buffer.next; context.buffer.next; Context.Buffer.ReadTo('*)'); Context.Buffer.Next; Context.Buffer.Next; end else Begin if mCache.length>0 then Begin writeln('Command is: "' + mCache + '"'); if getParserInstanceFor(lowercase(mCache),mParser) then Begin writeln('Parser ' + mParser.classname + ' is registered for keyword ' + mCache); try mParser.Parse; except on e: exception do raise Exception.Create(e.message); end; end else Raise Exception.create('Syntax error, unknown token:' + mCache); mCache :=''; exit; end else Raise Exception.Create('Syntax error, unsuspected character'); end; '/': If Context.buffer.compare('//') then begin Context.buffer.ReadToEOL(mTemp); end else if Context.buffer.compare('/*') then Begin Context.buffer.next; Context.buffer.Next; Context.buffer.ReadTo('*/'); end; '{': Begin if mCache.length>0 then Begin if getParserInstanceFor(lowercase(mCache),mParser) then Begin showmessage('over here!'); end else Raise Exception.create('Syntax error, unknown token:' + mCache); end else Raise Exception.Create('Syntax error, unexpected character'); end; '}': Begin end; else Begin mCache += Context.buffer.Current; end; end; if not context.buffer.next then break; end; writeln('Exiting CodeBlock parser at:' + mCache); end; //########################################################################### // TQTXContext //########################################################################### Constructor TQTXContext.Create; begin inherited Create; FBuffer:=TQTXBuffer.create; ApplicationObject:=TLPPApplicationObject.Create; end; Destructor TQTXContext.destroy; Begin FBuffer.free; ApplicationObject.free; inherited; end; //########################################################################### // TQTXBuffer //########################################################################### procedure TQTXBuffer.First; Begin If FData.length>0 then begin FLineNr:=1; FIndex:=0; end; end; procedure TQTXBuffer.SkipJunk; var mTemp: String; begin repeat case Current of ' ', #9 : Begin (* We treat TAB as space *) //if not next then //break; end; '/': Begin (* Skip C style remark *) if Compare('/*') then begin if readTo('*/') then Begin Next; Next; end else raise ENPPException.Create('Comment not closed error'); end else Begin (* Skip Pascal style remark *) if Compare('//') then begin if ReadToEOL(mTemp) then next else raise ENPPException.Create('Expected end of line error'); end; end; end; '(': Begin (* Skip pascal style remark *) if compare('(*') and not compare('(*)') then Begin if readTo('*)') then begin Next; Next; end else raise ENPPException.Create('Comment not closed error'); end else break; end; #13: begin // end; else begin Break; end; end; next; until EOF; end; Procedure TQTXBuffer.Clear; begin FData:=''; FLineNr:=1; FIndex:=0; end; procedure TQTXBuffer.LoadFromString(Text:String); Begin FLineNr:=1; FData:=trim(text); FData:=StrReplace(FData,#10,#13); FData:=StrReplace(FData,#13#13,#13); FIndex:=1; if length(FData)<1 then FIndex:=-1; end; function TQTXBuffer.getCurrent:String; Begin result:=FData[FIndex]; end; function TQTXBuffer.ReadWord(var Text:String):Boolean; begin result:=False; Text:=''; if not EOF then begin repeat if (current in ['A'..'Z','a'..'z','0'..'9']) then Text += current else break; until not next; result:=length(Text)>0; end; end; function TQTXBuffer.Compare(aText:String):Boolean; var mData: String; Begin result:=PeekAhead(length(aText),mData) and SameText(lowercase(mData),lowercase(aText)); end; function TQTXBuffer.PeekAhead(aCount:Integer;var Text:String):Boolean; var mPos: Integer; Begin result:=False; Text:=''; if not EOF then Begin mPos:=FIndex; try while aCount>0 do begin Text+=Current; if not Next then break; dec(aCount); end; result:=length(text)>0; finally FIndex:=mPos; end; end; end; function TQTXBuffer.ReadToEOL(var text:String):Boolean; Begin result:=ReadTo([#13,#10],text); end; function TQTXBuffer.ReadTo(aValue:String;var inner:String):Boolean; var mText: String; begin inner:=''; result:=False; aValue:=lowercase(aValue); if aValue.length>0 then begin repeat if PeekAhead(aValue.length,mText) then begin mText:=lowercase(mText); result:=SameText(mText,aValue); if result then break else inner += Current; end else inner += Current; Next; until EOF; end; end; function TQTXBuffer.readTo(aValue:String):Boolean; var mText: String; begin result:=False; aValue:=lowercase(aValue); if aValue.length>0 then begin repeat if PeekAhead(aValue.length,mText) then begin mText:=lowercase(mText); result:=SameText(mText,aValue); if result then break; end; if not Next then break; until EOF; (* repeat if PeekAhead(aValue.length,mText) and SameText(lowercase(mtext),aValue) then Begin result:=true; break; end else if not next then break; until EOF; *) end else Raise ENPPException.Create ('ReadTo() failed, invalid target value error'); end; function TQTXBuffer.ReadTo(Const aChars:Array of string; var text:String):Boolean; var x: Integer; Begin result:=False; text:=''; if aChars.Length>0 then begin for x:=FIndex to FData.length do Begin if (Current in aChars) then Begin result:=true; break; end else text+=Current; if not Next then break; end; end; end; Function TQTXBuffer.Back:Boolean; begin result:=FIndex>1; if result then dec(FIndex); end; function TQTXBuffer.Next:Boolean; begin Result:=FIndex<fdata.length; if="" result="" then="" begin="" inc(findex);="" (current="" in="" [#13,#10])="" inc(flinenr);="" end;="" function="" tqtxbuffer.bof:boolean;="" result:="FIndex=1;" tqtxbuffer.eof:boolean;="">=FData.Length; end; end.
N++ accessing services
Sometimes you come up with solutions which are a thing of pure beauty and elegance. Im falling in love with the simplicity of my N++ programming language, it’s robust boundaries which creates a situation where errors are quickly identified and dealt with.
Below is the syntax for using a web-service. Notice that setting up an WSDL endpoint is no more difficult than setting up a stdout pipe (command-line output).
program("service_test") { handshake { input { /* Consume WSDL Web-Service Endpoint */ service1 @ service("http://www.test.com/SOAP/myService/WSDL/", serviceType:SoapService); } output { myProcess @ process("self"); stdio @ pipe("stdout"); } } /* Execute RPC call */ execute (stdio,service1) { stdio:writeln("Calling webservice"); execute (*) { var int32 result = 0; set result = service1:getUserId("quartex","secret"); stdio:writelnF("UserID on server={0}", result); } fail (e) { stdio.writelnF("Calling soap service failed: {0}",e); proceed; } } /* Exit code for process */ set myProcess:exitCode = 0; }
For those of you interested in creating programming languages, I hope this wet’s your appetite to learn N++. At the moment the proof-of-concept is being written in Smart Pascal for nodeJS, but a native version running on Linux, Unix, Windows and OS X is the final product.
N++ is free to use. It is not open-source but created as a non-profit language which will be managed through an organization (more or less identical to php, perl and python).
To date, it’s the only language which is fully service oriented from the ground up.
Introduction to N++, a process oriented programming language
For some time now I have been working on implementing various programming languages; writing parsers, sub parsers and dictionaries. To date these languages have been well-known languages, like pascal, visual basic and (to some extent) a subset of C#.
Programming languages are typically born out of two schools of thought: pure necessity, or just good old fun. I remember reading about the E programming language when I was a teenager for instance, which is a language that looks quite impressive – but serves little purpose. To be blunt: it doesnt give you anything in particular in terms of actual tools or advantages over, say, any other language out there.
And then there are languages which are just a complete waste of time, like brainfuck and it’s derived mathematical madness. If your idea of fun is jumping butt-naked into an inverted number generator (read: PI) – then by all means, brainfuck is your language. But for the rest of us — perhaps something more tasteful and useful is in order..
Presenting N++
N++ is a language I am designing at the moment (N stands for Nandi, Shiva’s ox), or adding partial support for in the Quartex IDE. While I cant cover everything in a single blog post (and certantly not at this hour), I can present some concepts you may find interesting.
N is a language designed to collect, process and distribute data in large quantities; hence the strong power of the oxen Nandi. It belongs on the server, although there is no reason why you cant use it from inside a desktop application or as a service.
Let’s start with the classical hello world:
program ("hello_world") { handshake { input { void; } output { void; } } execute(*) { writeln("hello world"); } }
Let’s look at the program in broad terms. Since most of you are programmers you probably have some idea what is going on in the above code. The [pre]program()[/pre] block defines that this textfile is indeed the program file (as opposed to a module). All programs can only have a single program file, but as many modules as you like.
Next comes the handshake. Now this is a very handy (pun intended) subject. What it does is define the input the program can expect (or demand) as well as the output. In the above example we dont expect anything and we dont deliver anything, so both input and output is set to void.
The heart of this little program is within the execute block, where we use the method “writeln” to output the text “hello world”.
If you are wondering what the * character means inside the brackets, that section is called the data-view. In essence the * character means “what is known to the parent object”. In this case the parent is the program itself, which means that all global variables and objects should be regarded as known (or inherited into the scope).
Feedback loop
While hello-world is always fun, let’s do something a bit more complex. Let’s write a program that accepts X number of text arguments from the command-line, and then prints it back out again:
program ("feedback") { handshake { input { string arguments[]; } output { void; } } criteria (*) { arguments <> null; arguments.length > 0; } execute { process (arguments,item) { writeln(item); } } }
As you can see this example defined a typed handshake, where it expects to receive an string array as input. Since the program or module doesn’t produce anything, the output is again set to void (meaning “nothing”).
The criteria() section is probably the closest thing you will get to an IF statement in N++, what it does is validate X number of conditions, if they result in true the execute block is executed. You can also have an optional “fail” block to deal with scenarios where the criteria are not met.
The process() method is N++ variation of a FOR/NEXT loop. What it does is process the content of an object one-by-one and apply whatever code is inside it’s block on the data. As you can see from the parameters the arguments array from the input is the data we want to process, and as our second parameter we have a variable representing the current item.
Chainable criteria
One of the cool aspect of N++ is that you can chain these code blocks in various patterns. For instance you can chain process() and criteria() as such:
criteria { /* do not execute unless list has items */ list.length>0; } process (list,item) { /* do something to each item in the list */ } fail (e) { /* Criteria not met */ writeln(e); }
Or, apply criteria per item:
process (list,item) { writeln(item); } criteria { /* Skip all items which does not begin with "jo" */ item.firstname == "jo*"; }
Dynamic structures
While looping through data is fun, it’s not really something new. But being able to shape known data from various sources into new structures is extremely handy, especially for web services. Again it’s not a novelty – but here it’s a fundamental aspect of the language itself:
module ("data_export") { handshake { input { void; } output { object[]; } } define dbConn as database("myDB@localhost:8090;user:admin;password:adminpassword"); execute(dbconn) { collect ["users,"info"] from dbConn as data; set output = build(data,smAscending) { uid: users.UID; username: users.userHash; password: users.passHash; fullName: info.fullName; } } }
The above introduces a couple of new concepts. First there is the “define” keyword, which defined a data-source. In this case we associate “dbconn” as a database which we can obtain information from. By default N++ regards all types of data as either a single object, an array of object or a dictionary of object. You may also use an array as a dictionary – at which point the array object is transformed into a dictionary.
The collect() method does what the name implies, namely to collect data from a data-source, in this case we grab all the records from two tables (users and info) and stuff those into a named container (data).
And finally a magic method, namely the “build” function. In this case we create a completely new structure which will be sorted ascending — and then we map fields from the data-source(s) into the new structure.
When emitting JSON what this module returns is:
[
{
"uid": "1239094",
"userName": "AF965BF1274CCE1",
"passWord": "C009A6487BC120E",
"fullName": "Jon-Lennart Aasenden"
},
{
"uid": "987351",
"userName": "BF982737CD00A92",
"passWord": "AF965BF1274CCE1",
"fullName": "John Calvin"
},
{
"uid": "670941",
"userName": "09F9CA1BF982737",
"passWord": "4CCE1986CDE00AF",
"fullName": "Dave Jones"
}
]
The general idea here ofcourse, is that threading and multi-processor programming should be equally simple. If you have 10 million records and try to join them into a new structure like above, chances are it will take forever before the method returns. In fact, it may even break your database. But for ad-hoc amounts of data, threading it makes sense:
background-execute ("user-export",tpIdle, => (*) { /* Anonymous procedure callback handler */ signal.send([*].process.owner,MSG_NOTIFICATION,"$DB-EXPORT-READY"); } ) { collect ["users,"info"] from dbConn as data; set output = build(data,smAscending) { uid: users.UID; username: users.userHash; password: users.passHash; fullName: info.fullName; } } }
In short: when the data export is done and the data is delivered as the module output, a signal is sent to the owner process, of type “notification” with a string message saying that the export is complete.
Expanding the idea
The idea behind N++ is that a language designed exclusively to process data, working with stores (which in this case can be any linear flow of data-items, such as a database table, a folder with files, a text-list or any other vertical media) will be able to take shortcuts traditional languages cannot.
As of writing, N++ is barely out of the idea stage, with only the bare-bones parser and AST implemented, but already we are able to cut corners that are, in computing terms, quite costly.
Take the “IF” statement for instance. In any procedure involving more than 2 variables, you end up writing criteria testing at the beginning of the procedure:
function TMyService.DoSomething(a,b,c:Float):Float; Begin if (a>1.0) and ( (b<1.0) and (b>0.0) ) and (c>0.0) then begin result:=A * B - C; end else Begin //raise exception end; end;
Which can be more cleanly expressed through n++ direct approach:
method doSomething(demand a,b,c) { criteria { a > 1.0; b < 1.0; b > 0.0; c > 0.0; } execute { output float(a * b - c); } fail (e) { /* express error */ } } breach { /* parameter contract not met */ }
As you can see from the above code, N++ have some new concepts such as contract based interfaces. The interface of a procedure defined the syntax and expected input (as it does in all languages). This must not be confused with COM interfaces, as interface in this context is refering to header (as in C’s .h file). Object pascal doesn’t need a separate header file like C, since the syntax provides an interface section at the top of a source-file.
N++ doesnt even need that, and it can even (much like javascript) allow calling methods and modules without correct parameters. This is called a breach-of-contract and can be caught much like any error.
Contract based programming is not just limited to data-mapping, but is also used in parallell programming and cluster programming where invocation of a method can be done across the domain. All invocations are actually sent as signals within the cluster and workload can be distributed across several computers (nodes).
While that is not something I have even begun sculpting, putting it into the foundation of the language is important.
Well, I’m off to bed — it’s been a long day.
Delphi programmer wanted
The company I work for, Visma AS, which is one of the biggest and most prestigious companies in Scandinavia is looking for a Delphi developer.
Working remotely is ok, but with some criteria regarding availability during Norwegian office hours. Visma is a large and stable company offering competitive salaries and excellent benefits. As a company we are known for our high-standards.
Requirements
You must master Delphi well, not be afraid of low-level programming and be used to documenting your work. You must know your way around SQL server and preferably have some background in POS (point of sale) devices. And naturally, being fluent in English is a must.
Target platform is Win32/X86; Delphi application running on Windows XP Embedded.
Interested programmers can contact me on Facebook or send your CV to “jon.lennart.aasenden AT visma.com” – and I will forward your information to the Delphi department.
Sincerely
Jon Lennart Aasenden
Senior Software Engineer
Visma Retail Software AS
Delphi for dot net unit
I had a rather long discussion with several members of Delphi developer (Facebook) the other day, mostly in response to be becoming a full-time C# developer (and Delphi developer of-course, that’s not gonna change).
Although we started with debating C# and differences between native object-pascal versus the “curly languages” in general, I ended up saying something that clearly bugged a few, namely: We can actually implement the dot net framework as an alternative to the VCL, written in Delphi itself. There is no technical limitation against it, and it may even benefit object pascal in general – as younger developers are more familiar with dot net than they are the VCL or VJL.
As you probably guess that spawned some interesting comments (nothing bad, important to underline that) – most of the comments along the lines of the task being pointless, technically difficult or just plain impractical.
My reply to this is that you are all wrong (he said with a smile).
First of all, it is not more impractical to use clone of the most evolved, modern run-time-library (framework) than it is to use the VCL. Delphi is in reality suffering great injustice due to the in-grown identification of product, language and RTL as one and the same. In fact, many people are completely spellbound by the concept of object pascal being “Delphi”, that they cannot for their life imagine object pascal with a new RTL.
This is something I have had first-hand experience with, since I wrote the RTL for Smart Mobile Studio and was the first to experience the wave of feedback from both happy and unhappy users. Dont get me wrong, I absolutely love the VCL; It’s component model and class hiearcy has stood the test of time. It scales well, it’s agile – and all the other words we use to describe a living product.
Technical difficulties
Secondly, it is no more a technical challenge to implement the .net framework and use that instead of the VCL – than it would be to write the VCL to begin with. The factor which matters in this case, as it is with software development in general, is time.
But this statement does have some merit, since it’s only recently that object pascal (both Delphi and FPC) have evolved it’s RTTI functionality. This was a requirement to bring generics and “C++ and C#” type RTTI access and management to Delphi. And as always the FPC group followed suit – which we should be thankful for.
The only technical challenges that requires a fair bit of research and testing can be isolated in 3 groups:
- Fundamental differences in serialization
- Object life-time differences
- Native code lacks the ability to emit reflection and alter itself at runtime
Why do it at all?
And last but not least, to the question of why; The answer is that the dot net framework has quickly become the dominant framework. People like to believe that C++ is in the lead here, or even JavaScript which tops the code evolution charts, but that is not the case. The dot net framework is used by millions of programmers every single day, both young and old alike. No other framework has the same level of exposure; Microsoft has successfully installed their framework onto every Windows PC on the planet – and with their recently announced “open source” initiative — the dot net framework will become and important part of Unix, Linux and OS X.
Being able to offer customers a framework they already know – but with a twist: namely that it compiles to native code, fast, relentless and which is emitted as a single executable — is more effective than presenting something utterly alien to young programmers. The same can be done with ordinary .net or mono apps through the executable image tool – which generates a single .exe with no dependencies of your C# code.
Porting over important libraries from C# becomes substantially easier if at least a subset of the dot net framework can be mapped to C# in 1:1 fashion.
C# lacks many of the features which makes object pascal so attractive; A native dot net “clone” RTL, which would replace the VCL completely, would benefit from many of the already existing VCL classes — and also from the language features unique to object pascal.
Proof of concept
To make a long story short; I have implemented a handful of the fundamental dot net classes. I have only spent an afternoon on this, so dont expect miracles, but at least it implements the basic .net serialization engine (the .net framework actually has 3 engines for serialization, few people are aware of that).
And to be frank, it’s already so much more easier to use than vanilla VCL. Now dont start a flame-war because of that statement. I love the VCL and use it every single day — but one of the more time-consuming tasks I can think of, is to write persistent code (if your components expose fields of a non-standard datatype).
A second nail in the proverbial coffin is that Delphi’s persistence is exclusively binary. A lot of frameworks have alternatives for this, like mORMot, Remobjects and TMS’s Aurelius (which I really love, since it’s purely attribute based), but vanilla object pascal as delivered by Embarcadero still ships with TPersistent which havent evolved since it’s inception ages ago.
C# and other .net languages have built in serialization out of the box. It’s a very simple form of serialization, but due to it’s decoupled nature – where property identifier is separated from property data (so you can emit XML text to a binary medium) it’s very effective.
It’s also fully automatic, unless you explicitly turn it off. So under C# you can write a “normal” class as such:
/* Bog standard class. We inherit from ISerializable, and we also tag the class with the "Serializable" attribute */ [Serializable()] public class TMyClass: ISerializable { public int Value { get; set; } public string Name { get; set; } }
The above is identical to this object-pascal code. The VCL rule for persistence is that only published properties are automatically persisted by the VCL, and the property must be a non-complex type (e.g “standard datatypes like integer, string, double and so on). The problem is that you will only be able to load and in Delphi’s custom binary format, which makes it so much harder to work with high-end, industry standard, enterprise level solutions.
In the world of enterprise computing, methods typically take serialized objects as parameters. So instead of shipping in a ton of parameters – you ship in one string which contains an object exposing whatever properties you need.
Delphi does have such a system, buried deep with it’s RPC (remote procedure call) units — but the binary data cannot be made any better. It’s just base-64 encoded.
TMyClass = Class(TPersistent) private FValue:Integer; FName: String; Published Property Value:Integer read FValue write FValue; Property Name:String read FName write FName; End;
As you see from the C# code example, C# has adopted anonymous field declarations. Meaning that you dont define a property field (the actual field to hold a property’s value) by name. It remains anonymous and you simply access the exposed property name. This is a great time saver and it makes sense when you think about it. Smart Pascal implements this, so as of writing SMS is the only object-pascal compiler which allows you to write near identical pascal which maps directly to C#. It also does this without importing weird C++ syntax (let’s face it, generics sticks out like a sore thumb in Delphi). So Smart Pascal is in some ways closer to C# than BCPL; BCPL being the language pascal inherited many ideas from back in the 70’s.
Now when you want to serialize your object, which simply means that you are able to save all published properties automatically to XML, JSON, binary or whatever emitter is available, under C# you would just write:
void saveObjToStream(TMyClass mObject) { /* Use an XML serializer */ XmlSerializer ser = new XmlSerializer(typeof(TMyClass)); /* Setup target buffer */ MemoryStream mBuffer = new MemoryStream(); /* Setup our stream-writer */ TextWriter mWriter = new StreamWriter(mBuffer); /* Save object instance as XML to our memory stream */ ser.Serialize(mWriter, mObject); }
Reasonably straight forward; easy and effective. Delphi’s old TPersistent may be faster due to it’s binary format, but Delphi is suffering because of the binary-only technology which VCL represents. It would be easy to fix this for Embarcadero, but I guess they are focusing more on FMX these days.
Right, with the core .net “object” class implemented (see code below) we are now able to do something very similar:
procedure saveToStream(mObject:TMyClass) var mSerializer: TMSXMLSerializer; mBuffer: TMemoryStream; mWriter: TMSTextWriter; Begin mSerializer:=TMSXMLSerializer; end;
This is very different from how Delphi has traditionally dealt with serialization. TPersistent dispatches the job of writing data onto the component itself. This is very effective when dealing with large trees of objects and sub-objects (although stack hungry for very large structures). But be that as it may, Delphi’s TWriter and TReader is a binary affair from beginning to end. Which means Delphi serialization (as Embarcadero shipts it) cant play ball with the big-boys who exclusively use XML (even for parameters in DLL’s or ORMS).
Manual serialization
While the .net framework has the simple “automatic” serialization technique i demonstrated above, which is suitable for web services, databases and remote procedure calls — the .net framework actually has 3 different persistent serialization engines.
The second version is more hands-on and functions pretty much like Delphi’s TPersistent does. With one exception and that is a proxy object is used to register properties manually; This is where the TMSSerializationInfo class comes in.
When manually using this variation you simply derive a new class from TMSObject and implement the ISerializable interface. The system will then call on the GetObjectData() when needed to obtain a property dictionary, then that dictionary is used to either stream out RTTI information (the properties defined in the dictionary) or write properties to an instance.
Well, enough blabber from me — he is the “work in progress” code so you can see for yourself. I will probably finish it laster at some point, I am working on Smart Mobile Code at the moment.
unit qtx.system; interface uses System.Sysutils, System.Classes, System.rtti, System.TypInfo, System.Generics.Collections; type EQTXObject = Class(Exception); (* Exception classes *) EQTXObjectAlreadyRetained = Class(EQTXObject); EQTXObjectNotRetained = Class(EQTXObject); EQTXObjectRetained = Class(EQTXObject); EQTXObjectCloneFailed = Class(EQTXObject); EQTXObjectRTTIQueryFailed = Class(EQTXObject); (* Forward declarations *) TQTXObject = Class; TQTXPersistent = Class; TQTXSerializationInfo = Class; //TQTXObjectPropertyInfo = Class; //TQTXObjectPropertyInfoList = Class; TCharArray = packed array of char; TByteArray = packed array of byte; IDisposable = interface ['{56714944-F3D0-43C9-8C4B-F2F00BA5F83D}'] procedure Dispose; end; IRetainedObject = Interface ['{27B152DC-6553-4309-8C51-2B5C7D89A9EB}'] procedure RetainObject; procedure ReleaseObject; end; ICloneable = interface ['{6BAB94D0-32B9-4C4C-9D71-4C88AA9E6D0B}'] function Clone:TQTXObject; end; ISerializable = interface ['{FAD5405E-34B8-4264-8F8D-EE2A0D257213}'] function GetObjectData:TQTXSerializationInfo; end; TQTXObjectPropertyInfoList = Class; TQTXObjectPropertyInfo = Class(TObject) private FName: String; FDataType: TTypeKind; FParent: TQTXObjectPropertyInfoList; public Property PropertyName:String read FName write FName; property PropertyType:TTypeKind read FDataType write FDataType; function asString:String; constructor Create(Parent:TQTXObjectPropertyInfoList);virtual; end; TQTXObjectPropertyInfoList = class(TObjectList<TQTXObjectPropertyInfo>) private FInstance: TQTXObject; public Property Instance:TQTXObject read FInstance; function ToString:String;override; constructor Create(Instance:TQTXObject);reintroduce;virtual; end; (* IRTTIProvider = interface ['{6C3113DE-BAFD-46D1-9596-C1397991F02F}'] function queryPropertyInfo(var aList:TQTXObjectPropertyInfoList):Boolean; function getPropertyValue(aName:String;var data;buffLen:Integer):Boolean; end; *) ISomeThing = Interface function queryPropertyInfo(var aList:TQTXObjectPropertyInfoList):Boolean; function getPropertyValue(aName:String;var Data:PByte;buffLen:Integer):Boolean; end; TQTXObject = Class(TPersistent,IRetainedObject) strict private FRefCount: Integer; FRetained: Boolean; public function queryPropertyInfo(var list:TQTXObjectPropertyInfoList):Boolean; function getPropertyValue(aName:String; var data:Pointer; var buffLen:Integer):Boolean; strict protected procedure CloneProperties(aSource,aTarget:TQTXObject; Recursive:Boolean=False); class function ElfHash(const aData;aLength:Integer):LongWord;overload; class function ElfHash(const aText:String):LongWord;overload; strict protected Property RefCount:Integer read FRefCount; strict protected { IInterface } function _AddRef: Integer;virtual;stdcall; function _Release: Integer;virtual;stdcall; strict protected procedure RetainObject;virtual; procedure ReleaseObject;virtual; public function CloneMemberWise(var aClone):Boolean; procedure Finalize;virtual; class function ReferenceEquals(const ObjA,ObjB:TQTXObject):Boolean; class function GetHashCode:Longword;reintroduce; class function GetType:TClass; function ToString:String;override; Procedure Free;reintroduce;virtual; public function QueryInterface(const IID: TGUID; out Obj): HResult; virtual; stdcall; Procedure BeforeDestruction;Override; Procedure AfterConstruction;Override; end; (* See: http://msdn.microsoft.com/en-us/library/system.runtime.serialization.serializationinfo(v=vs.110).aspx For member info *) TQTXSerializationInfo = Class(TQTXObject) end; TQTXWriter = Class(TQTXObject) private FStream: TStream; strict protected procedure WriteBinary(const data;dataLen:Integer); public procedure Write(value:Boolean);overload;virtual; procedure Write(value:byte);overload;virtual; procedure Write(value:TByteArray);overload;virtual; procedure Write(value:char);overload;virtual; procedure Write(value:TCharArray);overload;virtual; procedure Write(value:String);overload;virtual; procedure Write(value:Integer);overload;virtual; procedure Write(value:word);overload;virtual; procedure Write(Value:Longword);overload;virtual; procedure Write(Value:double);overload;virtual; Procedure Write(Value:Int64);overload;virtual; constructor Create(target:TStream);virtual; destructor Destroy;Override; end; TQTXTextWriter = Class(TQTXWriter) strict protected Procedure WriteText(value:String); public procedure Write(value:Boolean);override; procedure Write(value:byte);override; procedure Write(value:TByteArray);override; procedure Write(value:char);override; procedure Write(value:TCharArray);override; procedure Write(value:String);override; procedure Write(value:Integer);override; procedure Write(value:word);override; procedure Write(Value:Longword);override; procedure Write(Value:double);override; Procedure Write(Value:Int64);override; end; TQTXReader = class(TQTXObject) end; TQTXTextReader = Class(TQTXReader) End; TQTXSerializer = Class(TQTXObject) public procedure Serialize(writer:TQTXWriter;const instance:TQTXObject);virtual;abstract; procedure DeSerialize(reader:TQTXReader;const instance:TQTXObject);virtual;abstract; end; TQTXXMLSerializer = Class(TQTXSerializer) public procedure Serialize(writer:TQTXWriter;const instance:TQTXObject);override; procedure DeSerialize(reader:TQTXReader;const instance:TQTXObject);override; end; TQTXBinarySerializer = Class(TQTXSerializer) End; TQTXPersistent = Class(TQTXObject,ICloneable,ISerializable) strict protected (* ICloneable *) function Clone:TQTXObject; strict protected (* ISerializable *) function GetObjectData:TQTXSerializationInfo;virtual; end; implementation class function TQTXObject.ElfHash(const aData;aLength:Integer):LongWord; var i: Integer; x: Cardinal; FSrc: PByte; Begin Result:=0; If aLength>0 then Begin FSrc:=@aData; for i:=1 to aLength do begin Result := (Result shl 4) + FSrc^; x := Result and $F0000000; if (x <> 0) then Result := Result xor (x shr 24); Result := Result and (not x); inc(FSrc); end; end; end; class function TQTXObject.ElfHash(const aText:String):LongWord; var FAddr: Pointer; FLen: Integer; Begin Result:=0; FLen:=Length(aText); If FLen>0 then Begin FAddr:=@aText[1]; Result:=ElfHash(FAddr^,FLen * Sizeof(Char)); end; end; //############################################################################# // TQTXObjectPropertyInfo //############################################################################# constructor TQTXObjectPropertyInfo.Create(Parent:TQTXObjectPropertyInfoList); begin inherited Create; FParent:=Parent; end; function TQTXObjectPropertyInfo.asString:String; var mStr: String; mInt: Integer; mInt64: Int64; mSize: Integer; mPTR: Pointer; mEnum: longword; mVar: Variant; begin setLength(result,0); if FParent<>NIL then begin if FParent.Instance<>NIL then Begin case FDataType of tkString, tkLString, tkUString: Begin mSize:=0; repeat inc(mSize,1024); setLength(mStr,mSize); fillchar(mStr[1],mSize,#0); mPTR:=pointer(@mStr[1]); until FParent.Instance.getPropertyValue(FName,mPTR,mSize); result:=QuotedStr(strPas(PChar(mPTR))); setLength(mStr,0); end; tkInteger: Begin mPTR:=@mInt; mSize:=SizeOf(Integer); FParent.Instance.getPropertyValue(FName,mPTR,mSize); result:=IntToStr(mInt); end; tkInt64: Begin mPTR:=@mInt64; mSize:=SizeOf(Int64); FParent.Instance.getPropertyValue(FName,mPTR,mSize); result:=IntToStr(mInt64); end; tkEnumeration: Begin mPTR:=@mEnum; mSize:=SizeOf(Longword); FParent.Instance.getPropertyValue(FName,mPTR,mSize); if mSize=SizeOf(Boolean) then result:=boolToStr(PBoolean(mPTR)^,true) else Begin result:='[Enumeration]'; end; end; tkVariant: Begin mPTR:=@mVar; mSize:=SizeOf(Variant); FParent.Instance.getPropertyValue(FName,mPTR,mSize); result:=string(mVar); end; end; end; end; end; //############################################################################# // TQTXObjectPropertyInfoList //############################################################################# constructor TQTXObjectPropertyInfoList.Create(Instance:TQTXObject); Begin inherited Create(True); FInstance:=Instance; end; function TQTXObjectPropertyInfoList.ToString:String; var x: Integer; Begin setLength(result,0); for x:=0 to Count-1 do Begin result:=result + Items[x].PropertyName + '=' + items[x].asString; if x<(count-1) then result:=result + #13; end; end; //############################################################################# // TQTXXMLSerializer //############################################################################# procedure TQTXXMLSerializer.Serialize (writer:TQTXWriter;const instance:TQTXObject); Begin if assigned(writer) then begin if assigned(instance) then Begin end; end; end; procedure TQTXXMLSerializer.DeSerialize (reader:TQTXReader;const instance:TQTXObject); Begin end; //############################################################################# // TQTXTextWriter //############################################################################# Procedure TQTXTextWriter.WriteText(value:String); Begin if length(value)>0 then Begin Value:=Value + #13#10; FStream.Write(value[1],length(value) * SizeOf(Char)); end; end; procedure TQTXTextWriter.Write(value:Boolean); Begin WriteText(BoolToStr(value,true)); end; procedure TQTXTextWriter.Write(value:byte); Begin WriteText('$' + IntToHex(Value,2)); end; procedure TQTXTextWriter.Write(value:TByteArray); var x: Integer; Begin if length(value)>0 then for x:=low(value) to high(value) do Write(Value[x]); end; procedure TQTXTextWriter.Write(value:char); Begin FStream.Write(Value,SizeOf(Char)); end; procedure TQTXTextWriter.Write(value:TCharArray); var x: Integer; Begin if length(Value)>0 then for x:=low(Value) to high(Value) do FStream.Write(Value[x],SizeOf(Char)); end; procedure TQTXTextWriter.Write(value:String); Begin WriteText(Value); end; procedure TQTXTextWriter.Write(value:Integer); Begin WriteText(IntToStr(Value)); end; procedure TQTXTextWriter.Write(value:word); Begin WriteText('$' + IntToHex(Value,4)); end; procedure TQTXTextWriter.Write(Value:Longword); Begin WriteText('$' + IntToHex(Value,8)); end; procedure TQTXTextWriter.Write(Value:double); Begin WriteText(FloatToStr(Value)); end; Procedure TQTXTextWriter.Write(Value:Int64); Begin WriteText(IntToStr(value)); end; //############################################################################# // TQTXWriter //############################################################################# constructor TQTXWriter.Create(target:TStream); Begin inherited Create; FStream:=target; end; destructor TQTXWriter.Destroy; Begin inherited; end; procedure TQTXWriter.WriteBinary(const data;dataLen:Integer); Begin FStream.Write(data,dataLen); end; procedure TQTXWriter.Write(value:Boolean); Begin WriteBinary(Value,sizeOf(Value)); end; procedure TQTXWriter.Write(value:byte); Begin WriteBinary(Value,sizeOf(Value)); end; procedure TQTXWriter.Write(value:TByteArray); Begin if length(value)>0 then WriteBinary(value,length(value)); end; procedure TQTXWriter.Write(value:char); Begin WriteBinary(Value,sizeOf(Value)); end; procedure TQTXWriter.Write(value:TCharArray); Begin if length(value)>0 then WriteBinary(Value,SizeOf(Char) * Length(Value)); end; procedure TQTXWriter.Write(value:String); Begin WriteBinary(Value,sizeOf(Value)); end; procedure TQTXWriter.Write(value:Integer); Begin WriteBinary(Value,sizeOf(Value)); end; procedure TQTXWriter.Write(value:word); Begin WriteBinary(Value,sizeOf(Value)); end; procedure TQTXWriter.Write(Value:Longword); Begin WriteBinary(Value,sizeOf(Value)); end; procedure TQTXWriter.Write(Value:double); Begin WriteBinary(Value,sizeOf(Value)); end; Procedure TQTXWriter.Write(Value:Int64); Begin WriteBinary(Value,sizeOf(Value)); end; //############################################################################# // TQTXPersistent //############################################################################# function TQTXPersistent.GetObjectData:TQTXSerializationInfo; begin result:=TQTXSerializationInfo.Create; end; function TQTXPersistent.Clone:TQTXObject; var mClass: TClass; begin result:=NIL; mClass:=getType; if mClass<>NIl then Begin (* Create instance *) result:=TQTXObject(mClass.Create); (* Do a recursive "deep-copy" of the object properties *) try cloneProperties(self,result,true); except on e: exception do begin freeAndNIL(result); Raise EQTXObjectCloneFailed.CreateFmt ('Failed to clone %s, method %s threw exception %s with message %s', [self.ClassType.ClassName,'Clone',e.ClassName,e.Message]); end; end; end; end; //############################################################################# // TQTXObject //############################################################################# Procedure TQTXObject.AfterConstruction; begin inherited; AtomicDecrement(FRefCount); end; Procedure TQTXObject.BeforeDestruction; Begin if RefCount <> 0 then Error(reInvalidPtr); Finalize; inherited; end; Procedure TQTXObject.Free; Begin if FRetained then Raise EQTXObjectRetained.Create ('Object is retained and cannot be released error'); Inherited free; end; function TQTXObject._AddRef: Integer; begin Result := AtomicIncrement(FRefCount); end; procedure TQTXObject.RetainObject; Begin (* Prevent automatic release through self-increment *) if not FRetained then FRetained:=_addRef>0 else raise EQTXObjectAlreadyRetained.Create ('Object is already marked as retained error'); end; procedure TQTXObject.ReleaseObject; Begin if FRetained then _release else raise EQTXObjectNotRetained.Create ('Object is not retained error'); end; function TQTXObject._Release: Integer; begin (* Note: Delphi calls destroy directly, but since we want to be in tune with future possible changes to the VCL/FMX where free is expanded, I decided to invoke that instead *) Result := AtomicDecrement(FRefCount); if result<1 then free; end; function TQTXObject.QueryInterface(const IID: TGUID;out Obj): HResult; const E_NOINTERFACE = HResult($80004002); begin if GetInterface(IID, Obj) then Result := 0 else Result := E_NOINTERFACE; end; (* This is the dot net variation of "beforedestruction". I have included it for completeness and compatability only. It is invoked from beforedestruction. Also, this is where IDisposable is checked for *) Procedure TQTXObject.Finalize; var mAccess: IDisposable; begin (* Release unmanaged data *) if getInterface(IDisposable,mAccess) then mAccess.Dispose; end; function TQTXObject.ToString:String; Begin result:=self.ClassType.ClassName; end; class function TQTXObject.ReferenceEquals(const ObjA,ObjB:TQTXObject):Boolean; Begin result:=(objA<>NIL) and (objB<>NIL) and (objA = objB); end; class function TQTXObject.GetHashCode:longword; begin result:=TQTXObject.ElfHash(ClassName); end; class function TQTXObject.GetType:TClass; var ctx: TRttiContext; objType: TRttiType; begin result:=NIL; ctx := TRttiContext.Create; objType := ctx.GetType(ClassInfo); if (objType<>NIL) and (objType.AsInstance<>NIL) then result:=objType.AsInstance.ClassType; end; function TQTXObject.getPropertyValue(aName:String; var Data:Pointer; var buffLen:Integer):Boolean; var numProps, I : Integer; props: PPropList; PropInfo: PPropInfo; mInfo: TQTXObjectPropertyInfo; mText: String; mLen: Integer; Begin result:=False; if (Data<>NIL) and (BuffLen>0) then Begin numProps := GetPropList(self, props); try if numProps>0 then begin for i:=0 to numProps-1 do begin PropInfo := props^[I]; if sameText(String(PropInfo^.Name),aName) then Begin case propInfo^.PropType^.Kind of tkInteger: Begin if BuffLen>=SizeOf(Integer) then Begin Integer(data):=GetOrdProp(self,propinfo); BuffLen:=SizeOf(Integer); end; break; end; tkChar: begin if BuffLen>=SizeOf(char) then Begin PChar(data)^:=Char ( GetOrdProp(self,propinfo) ); BuffLen:=SizeOf(Char); end; break; end; tkEnumeration, tkSet, tkWChar: Begin if PropInfo^.PropType^ = TypeInfo(boolean) then Begin if BuffLen>=SizeOf(Boolean) then begin PBoolean(Data)^:=Boolean(GetOrdProp(self,propinfo)); BuffLen:=SizeOf(Boolean); break; end; end; if BuffLen>=SizeOf(longword) then Begin PLongword(data)^:=GetOrdProp(self,propinfo); BuffLen:=SizeOf(Longword); end; break; end; tkFloat: Begin if BuffLen>=SizeOf(Double) then Begin PDouble(data)^:=GetOrdProp(self,propinfo); BuffLen:=SizeOf(Double); end; break; end; tkString, tkLString, tkUString: begin mText:=GetStrProp(self,propinfo); mLen:=length(mText) * SizeOf(Char); if BuffLen>=mLen then Begin move(mText[1],data^,mLen); BuffLen:=mLen; end; break; end; tkInt64: Begin if BuffLen>=SizeOf(Char) * Length(mText) then Begin PInt64(data)^:=GetInt64Prop(self,propinfo); BuffLen:=SizeOf(Int64); end; break; end; tkVariant: begin if BuffLen>=SizeOf(variant) then Begin PVariant(Data)^:=getVariantProp(self,PropInfo); BuffLen:=SizeOf(Variant); end; break; end; (* tkInterface: begin break; end; tkMethod: Begin break; end; *) end; end; end; result:=(BuffLen>0); end; finally FreeMem(props); end; end; end; function TQTXObject.queryPropertyInfo (var list:TQTXObjectPropertyInfoList):Boolean; var numProps, I : Integer; props: PPropList; PropInfo: PPropInfo; mInfo: TQTXObjectPropertyInfo; Begin list:=NIL; result:=False; numProps := GetPropList(self, props); try if numProps>0 then begin list:=TQTXObjectPropertyInfoList.Create(self); for i:=0 to numProps-1 do begin PropInfo := props^[i]; if not (PropInfo^.PropType^.Kind in [tkClass,tkArray,tkRecord,tkDynArray]) then Begin mInfo:=TQTXObjectPropertyInfo.Create(list); mInfo.PropertyName:=propInfo^.Name; mInfo.PropertyType:=PropInfo^.PropType^.Kind; list.Add(mInfo); end; end; if list.Count<1 then freeAndNIL(list); result:=list<>NIL; end; finally FreeMem(props); end; end; procedure TQTXObject.CloneProperties(aSource,aTarget:TQTXObject; Recursive:Boolean=False); var numProps, I : Integer; props: PPropList; PropInfo: PPropInfo; src: TObject; dst: TObject; Begin numProps := GetPropList(aSource, props ); Try For I := 0 To numProps - 1 Do Begin PropInfo := props^[I]; Case PropInfo^.PropType^.Kind Of tkInteger, tkChar, tkEnumeration, tkSet, tkWChar: SetOrdProp(aTarget,propinfo,GetOrdProp(aSource,propinfo)); tkFloat: SetFloatProp(aTarget,propinfo,GetFloatProp(aSource,propinfo)); tkString, tkLString, tkUString: SetStrProp( aTarget, propinfo,GetStrProp( aSource, propinfo)); tkWString: SetWideStrProp(aTarget,propinfo,GetWideStrProp(aSource,propinfo)); tkMethod: SetMethodProp(aTarget,propinfo,GetMethodProp(aSource,propinfo)); tkInt64: SetInt64Prop(aTarget,propinfo,GetInt64Prop(aSource,propinfo)); tkVariant: SetVariantProp(aTarget,propinfo,GetVariantProp(aSource,propinfo)); tkInterface: SetInterfaceProp(aTarget,propinfo,GetInterfaceProp(aSource,propinfo)); tkClass: Begin if Recursive then Begin src := GetObjectProp( aSource, propinfo ); If Assigned( src ) Then Begin If src Is TComponent Then SetObjectProp( aTarget, propinfo, src ) else If src Is TPersistent Then Begin if src<>self then begin dst := GetObjectProp( aTarget, propinfo, TPersistent); if dst<>self then begin If Assigned( dst ) Then TPersistent( dst ).Assign( TPersistent(src)); end; end; End; End; end; End; tkArray, tkRecord, tkDynArray: begin end end; end; Finally FreeMem( props ); End; end; function TQTXObject.CloneMemberWise(var aClone):Boolean; var mClass: TClass; begin NativeInt(aClone):=0; result:=False; mClass:=getType; if mClass<>NIl then Begin TQTXObject(pointer(aClone)):=TQTXObject(mClass.Create); (* Do a recursive "deep-copy" of the object properties *) try cloneProperties(self,TQTXObject(pointer(aClone)),false); except on e: exception do begin freeAndNIL(result); Raise EQTXObjectCloneFailed.CreateFmt ('Failed to clone %s, method %s threw exception %s with message %s', [self.ClassType.ClassName,'CloneMemberWise',e.ClassName,e.Message]); end; end; //cloneProperties(self,TQTXObject(pointer(aClone))); result:=NativeInt(aClone)<>0; end; end; end.
Dot net framework for Delphi?
One idea that has been lurking in the back of my head regarding the QTX framework, is that we could – actually, simply implements a light version of the dot net framework instead.
I realize that this may be anathema to many people, and the first question will no doubt be “why” rather than “how”, but it does make sense from a purely strategic point of view.
- It makes porting software from C# easier
- To some degree simplified coding style (read: easier)
- Many powerful concepts which object pascal lacks
- Benefit of both worlds: object pascal and the dot net theorem
To see if this would work even on a conceptual level I decided to implement the System.Object class in object pascal. To get the benefit of garbage collection I have derived the base class from TPersistent and implemented IInterface. Destruction of un-managed resources are under the dot net framework ensured destruction through IDisposable, which I also added.
As most people know, objects that uses reference-counting can be harder to work with under native languages. If you place them in an object-list and expects them to survive you are in for a surprise, because even though TObjectList retains the pointer the instance is released.
To avoid this I have implemented the IRetainable interface. So remember to invoke the RetainObject() method. Calling RetainObject() ensures that an element self-references; read: will never automatically release itself because the reference counter never reaches zero.
Well, here is the unit. If anyone else is interested in implementing the dot net framework in Delphi then let me know, we could setup a repository and work together on it.
unit qtx.system; interface uses System.Sysutils, System.Classes, System.rtti, System.TypInfo; type EQTXObject = Class(Exception); (* Exception classes *) EQTXObjectAlreadyRetained = Class(EQTXObject); EQTXObjectNotRetained = Class(EQTXObject); EQTXObjectRetained = Class(EQTXObject); EQTXObjectCloneFailed = Class(EQTXObject); (* Forward declarations *) TQTXObject = Class; TQTXPersistent = Class; TQTXSerializationInfo = Class; IDisposable = interface ['{56714944-F3D0-43C9-8C4B-F2F00BA5F83D}'] procedure Dispose; end; IRetainedObject = Interface ['{27B152DC-6553-4309-8C51-2B5C7D89A9EB}'] procedure RetainObject; procedure ReleaseObject; end; ICloneable = interface ['{6BAB94D0-32B9-4C4C-9D71-4C88AA9E6D0B}'] function Clone:TQTXObject; end; ISerializable = interface ['{FAD5405E-34B8-4264-8F8D-EE2A0D257213}'] function GetObjectData:TQTXSerializationInfo; end; TQTXObject = Class(TPersistent,IRetainedObject) strict private FRefCount: Integer; FRetained: Boolean; strict protected procedure CloneProperties(aSource,aTarget:TQTXObject; Recursive:Boolean=False); strict protected Property RefCount:Integer read FRefCount; strict protected { IInterface } function _AddRef: Integer;virtual;stdcall; function _Release: Integer;virtual;stdcall; strict protected procedure RetainObject;virtual; procedure ReleaseObject;virtual; public function CloneMemberWise(var aClone):Boolean; procedure Finalize;virtual; class function ReferenceEquals(const ObjA,ObjB:TQTXObject):Boolean; class function GetHashCode:Longword;reintroduce; class function GetType:TClass; function ToString:String;override; Procedure Free;reintroduce;virtual; public function QueryInterface(const IID: TGUID; out Obj): HResult; virtual; stdcall; Procedure BeforeDestruction;Override; Procedure AfterConstruction;Override; end; (* See: http://msdn.microsoft.com/en-us/library/ system.runtime.serialization.serializationinfo(v=vs.110).aspx For member info *) TQTXSerializationInfo = Class(TQTXObject) end; TQTXPersistent = Class(TQTXObject,ICloneable,ISerializable) strict protected (* ICloneable *) function Clone:TQTXObject; strict protected (* ISerializable *) function GetObjectData:TQTXSerializationInfo;virtual; end; implementation uses brage; //############################################################################# // TQTXPersistent //############################################################################# function TQTXPersistent.GetObjectData:TQTXSerializationInfo; begin result:=TQTXSerializationInfo.Create; end; function TQTXPersistent.Clone:TQTXObject; var mClass: TClass; begin result:=NIL; mClass:=getType; if mClass<>NIl then Begin (* Create instance *) result:=TQTXObject(mClass.Create); (* Do a recursive "deep-copy" of the object properties *) try cloneProperties(self,result,true); except on e: exception do begin freeAndNIL(result); Raise EQTXObjectCloneFailed.CreateFmt ('Failed to clone %s, method %s threw exception %s with message %s', [self.ClassType.ClassName,'Clone',e.ClassName,e.Message]); end; end; end; end; //############################################################################# // TQTXObject //############################################################################# Procedure TQTXObject.AfterConstruction; begin inherited; AtomicDecrement(FRefCount); end; Procedure TQTXObject.BeforeDestruction; Begin if RefCount <> 0 then Error(reInvalidPtr); Finalize; inherited; end; Procedure TQTXObject.Free; Begin if FRetained then Raise EQTXObjectRetained.Create ('Object is retained and cannot be released error'); Inherited free; end; function TQTXObject._AddRef: Integer; begin Result := AtomicIncrement(FRefCount); end; procedure TQTXObject.RetainObject; Begin (* Prevent automatic release through self-increment *) if not FRetained then FRetained:=_addRef>0 else raise EQTXObjectAlreadyRetained.Create ('Object is already marked as retained error'); end; procedure TQTXObject.ReleaseObject; Begin if FRetained then _release else raise EQTXObjectNotRetained.Create ('Object is not retained error'); end; function TQTXObject._Release: Integer; begin (* Note: Delphi calls destroy directly, but since we want to be in tune with future possible changes to the VCL/FMX where free is expanded, I decided to invoke that instead *) Result := AtomicDecrement(FRefCount); if result<1 then free; end; function TQTXObject.QueryInterface(const IID: TGUID;out Obj): HResult; const E_NOINTERFACE = HResult($80004002); begin if GetInterface(IID, Obj) then Result := 0 else Result := E_NOINTERFACE; end; (* This is the dot net variation of "beforedestruction". I have included it for completeness and compatability only. It is invoked from beforedestruction. Also, this is where IDisposable is checked for *) Procedure TQTXObject.Finalize; var mAccess: IDisposable; begin (* Release unmanaged data *) if getInterface(IDisposable,mAccess) then mAccess.Dispose; end; function TQTXObject.ToString:String; Begin result:=self.ClassType.ClassName; end; class function TQTXObject.ReferenceEquals(const ObjA,ObjB:TQTXObject):Boolean; Begin result:=(objA<>NIL) and (objB<>NIL) and (objA = objB); end; class function TQTXObject.GetHashCode:longword; begin result:=brage.TBRBuffer.ElfHash(ClassName); end; class function TQTXObject.GetType:TClass; var ctx: TRttiContext; objType: TRttiType; begin result:=NIL; ctx := TRttiContext.Create; objType := ctx.GetType(ClassInfo); if (objType<>NIL) and (objType.AsInstance<>NIL) then result:=objType.AsInstance.ClassType; end; procedure TQTXObject.CloneProperties(aSource,aTarget:TQTXObject; Recursive:Boolean=False); var numProps, I : Integer; props: PPropList; PropInfo: PPropInfo; src: TObject; dst: TObject; Begin numProps := GetPropList(aSource, props ); Try For I := 0 To numProps - 1 Do Begin PropInfo := props^[I]; Case PropInfo^.PropType^.Kind Of tkInteger, tkChar, tkEnumeration, tkSet, tkWChar: SetOrdProp(aTarget,propinfo,GetOrdProp(aSource,propinfo)); tkFloat: SetFloatProp(aTarget,propinfo,GetFloatProp(aSource,propinfo)); tkString, tkLString, tkUString: SetStrProp( aTarget, propinfo,GetStrProp( aSource, propinfo)); tkWString: SetWideStrProp(aTarget,propinfo,GetWideStrProp(aSource,propinfo)); tkMethod: SetMethodProp(aTarget,propinfo,GetMethodProp(aSource,propinfo)); tkInt64: SetInt64Prop(aTarget,propinfo,GetInt64Prop(aSource,propinfo)); tkVariant: SetVariantProp(aTarget,propinfo,GetVariantProp(aSource,propinfo)); tkInterface: SetInterfaceProp(aTarget,propinfo,GetInterfaceProp(aSource,propinfo)); tkClass: Begin if Recursive then Begin src := GetObjectProp( aSource, propinfo ); If Assigned( src ) Then Begin If src Is TComponent Then SetObjectProp( aTarget, propinfo, src ) else If src Is TPersistent Then Begin if src<>self then begin dst := GetObjectProp( aTarget, propinfo, TPersistent); if dst<>self then begin If Assigned( dst ) Then TPersistent( dst ).Assign( TPersistent(src)); end; end; End; End; end; End; tkArray, tkRecord, tkDynArray: begin end end; end; Finally FreeMem( props ); End; end; function TQTXObject.CloneMemberWise(var aClone):Boolean; var mClass: TClass; begin NativeInt(aClone):=0; result:=False; mClass:=getType; if mClass<>NIl then Begin TQTXObject(pointer(aClone)):=TQTXObject(mClass.Create); (* Do a recursive "deep-copy" of the object properties *) try cloneProperties(self,TQTXObject(pointer(aClone)),false); except on e: exception do begin freeAndNIL(result); Raise EQTXObjectCloneFailed.CreateFmt ('Failed to clone %s, method %s threw exception %s with message %s', [self.ClassType.ClassName,'CloneMemberWise',e.ClassName,e.Message]); end; end; //cloneProperties(self,TQTXObject(pointer(aClone))); result:=NativeInt(aClone)<>0; end; end; end.
Enjoy!
TDBF works like a charm
Took two minutes to check out TDBF, the free open-source embedded database for Delphi (out of many). Discovered that it actually ships with Lazarus. I did a clean install of Lazarus/FPC on one of my mac’s at work (old macbook) just for fun – and you will find it pre-installed in the database tab.
Right. To get it working all you need to do is the following:
procedure TForm1.Button1Click(Sender: TObject); var x: Integer; mstart: TDateTime; mField: TField; begin dbx.FilePath:=getMyDocsPath; dbx.CreateTable; dbx.Open; mStart:=now; mField:=dbx.fieldByname('name'); for x:=1 to 100000 do begin dbx.Append; mField.AsString:='Name #' + IntToStr(x); dbx.post; end; caption:='Time:' + TimeToStr(now-mStart); dbx.First; listbox1.items.BeginUpdate; repeat listbox1.items.Add(dbx.fieldByName('name').asString); dbx.Next; if listbox1.items.count>100 then break; until dbx.EOF; listbox1.items.EndUpdate; end;
That code will create a new database (remember to add ID and Name fields to the field-def collection property of the component).
The function getMyDocsPath looks like this:
function getMyDocsPath:String; Begin result:='/Users/' + getCurrentUser + '/Documents/'; end;
Note: The above code is for Unix (OS X) only, but should be trivial to figure out for Windows or Linux.
Speed
Well I only tested it on my ancient mac, but I was able to stuff 100.000 records into the flat-file in 16 seconds. This includes (as you can see above) a post for each insert. The time should be a lot better by caching, say, 1000 records in memory before commiting the data to disk.
Either way — TDBF is a great little database if you have a small project, or even if you just want to store options and/or user-data like they do in dot net.
Check out the code from sourceforge here: http://tdbf.sourceforge.net/
You must be logged in to post a comment.