Pascal Server Pages – Pascal Script

Without getting too technical, I would define a Pascal Server Page (PSP) as a dynamic web page containing embedded Pascal Script (PS) code.  When a web request is made, the PS code needs to be executed (interpreted) in the server side and outputted into the proper format (HTML, XML, JSON, text, etc). A PSP is commonly stored as a text file in the Web Server and it could be a mixture of PS code plus any other static content.

This is an example of PSP:

<html>
  <head>
    <title>This is a Pascal Server Page</title>
  </head>
  <body>
    <% begin
         Write('Hello World');
       end.
    %>
    <p>I am going to use Pascal Script to write a few numbers...</p>
    <% var
         i: Integer;
       begin
         for i:=1 to 10 do
           Writeln(i); 
       end.
    %>
  </body>
</html>

The code above is an HTML armature containing some PS code. The PS code has been isolated within the “<%” and “%>” tokens. The PS code is executed in the server and the output (if any) is embedded into the HTML template.

So, if a browser asks for the page above, it will actually get plain HTML code as the one below:

<html>
  <head>
    <title>This is a Pascal Server Page</title>
  </head> 
  <body>   
    Hello World
    <p>I am going to use Pascal Script to write a few numbers...</p>
    1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br>10<br>
  </body>
</html>

This is all good. The only problem is that the PS code is not going to be magically executed. We need a server side component to do the PS interpretation.

I have seen a couple of intents to build such server side component in the Internet. Anyhow, I bring you my own proposal: create a Web Broker application with Pascal Scripting capabilities. To provide the Web Broker application with the scripting capabilities, I will use Pascal Script from RemObjects. You need to download and install Pascal Script if you want to try my code. 

The workflow goes as follows:
  1. The Web Broker application receives a Web Request.
  2. The Web Broker application finds the corresponding Pascal Server Page and loads its content to a buffer variable.
  3. The content of the buffer variable is parsed in order to find the PS tokens (I will use RegEx to do the parsing).
  4. Each PS block is compiled to Bytecode and then executed in the server. (I will use the Pascal Script library from RemObjects for this purpose). 
  5. The output generated from the execution of each PS block replaces its corresponding “<%......%>” block.
  6. The Web Broker app serves the response.
I developed a VCL standalone Web Broker application as a proof of concept (it could be an ISAPI dll as well). See it in action in the following video:



That application is just a prototype. I really believe that we could build a robust server side component to leverage enterprise Pascal Server Pages. I used Web Broker in this example, but we could also build Apache Modules with Free Pascal.

I am posting below the code of the TWebModule1 class, which is the core of the Web Broker app. The full source code and executable can be downloaded here. (the code was compiled with Delphi XE2). Note that the code is somewhat messy; this was taken directly from my sandbox. Ah, I copy-pasted (and adjusted) the Pascal Script routines from this example: Introduction to Pascal Script.


unit WebModuleUnit1;


interface


uses System.SysUtils, System.Classes, Web.HTTPApp,
     RegularExpressions;


type
  TWebModule1 = class(TWebModule)
    procedure WebModule1DefaultHandlerAction(Sender: TObject;
      Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
  private
    { Private declarations }
  public
    { Public declarations }
    function LoadStrFromFile(aFilename: string): string;
    function ProducePage(aContent: string): string;
    function ExecPascalScript(const Match: TMatch): string;
    function CompileScript(Script: AnsiString; out Bytecode, Messages: AnsiString): Boolean;
    function RunCompiledScript(Bytecode: AnsiString; out RuntimeErrors: AnsiString): Boolean;
  end;


var
  WebModuleClass: TComponentClass = TWebModule1;


  //ScriptOutput is not thread safe.
  //ScriptOutput is a global variable.
  //We should avoid global variables.
  //TODO: Find a better way to store the script output
  ScriptOutput: string;


implementation


uses
  uPSCompiler, uPSRuntime;


{$R *.dfm}


procedure Write(P1: Variant);
begin
  //This try...except is ugly.
  //TODO: Use a conditional checking instead
  try
    ScriptOutput:= ScriptOutput + String(P1);
  except
    ScriptOutput:= '';
  end;
end;


procedure Writeln(P1: Variant);
begin
  Write(P1);
  ScriptOutput:= ScriptOutput + '</br>';
end;


function TWebModule1.LoadStrFromFile(aFilename: string): string;
begin
  Result:= '';
  if not FileExists(aFilename) then Exit;


  with TStringStream.Create do
  try
    LoadFromFile(aFilename);
    Result:= DataString;
  finally
    Free;
  end;
end;


function TWebModule1.ProducePage(aContent: string): string;
var
  RegEx: TRegEx;
begin
  ScriptOutput:= '';
  aContent:= StringReplace(aContent, #13#10, '', [rfReplaceAll]);
  RegEx.Create('\<\%(.)*?\%\>');
  Result:= regex.Replace(aContent, ExecPascalScript);
end;


function TWebModule1.ExecPascalScript(const Match: TMatch): string;
var
  Bytecode,
  Messages,
  RuntimeErrors: AnsiString;
  PS: string;
begin
  Result:= '';
  Bytecode:= '';
  ScriptOutput:= '';
  PS:= Match.Value;
  PS:= StringReplace(PS, '<%', '', []);
  PS:= StringReplace(PS, '%>', '', []);
  if CompileScript(PS, Bytecode, Messages) then
    if RunCompiledScript(Bytecode, RuntimeErrors) then
      Result:= ScriptOutput;
end;


function ExtendCompiler(Compiler: TPSPascalCompiler; const Name: AnsiString): Boolean;
begin
  Result := True;
  try
    Compiler.AddDelphiFunction('procedure Writeln(P1: Variant);');
    Compiler.AddDelphiFunction('procedure Write(P1: Variant);');
  except
    Result := False; // will halt compilation
  end;
end;


function TWebModule1.CompileScript(Script: AnsiString; out Bytecode, Messages: AnsiString): Boolean;
var
  Compiler: TPSPascalCompiler;
  i: Integer;
begin
  Bytecode:= '';
  Messages:= '';


  Compiler:= TPSPascalCompiler.Create;
  Compiler.OnUses:= ExtendCompiler;
  try
    Result:= Compiler.Compile(Script) and Compiler.GetOutput(Bytecode);
    for i:= 0 to Compiler.MsgCount - 1 do
      if Length(Messages) = 0 then
        Messages:= Compiler.Msg[i].MessageToString
      else
        Messages:= Messages + #13#10 + Compiler.Msg[i].MessageToString;
  finally
    Compiler.Free;
  end;
end;


procedure ExtendRuntime(Runtime: TPSExec; ClassImporter: TPSRuntimeClassImporter);
begin
  Runtime.RegisterDelphiMethod(nil, @Writeln, 'Writeln', cdRegister);
  Runtime.RegisterDelphiMethod(nil, @Write, 'Write', cdRegister);
end;


function TWebModule1.RunCompiledScript(Bytecode: AnsiString; out RuntimeErrors: AnsiString): Boolean;
var
  Runtime: TPSExec;
  ClassImporter: TPSRuntimeClassImporter;
begin
  Runtime:= TPSExec.Create;
  ClassImporter:= TPSRuntimeClassImporter.CreateAndRegister(Runtime, false);
  try
    ExtendRuntime(Runtime, ClassImporter);
    Result:= Runtime.LoadData(Bytecode)
          and Runtime.RunScript
          and (Runtime.ExceptionCode = erNoError);
    if not Result then
      RuntimeErrors:=  PSErrorToString(Runtime.LastEx, '');
  finally
    ClassImporter.Free;
    Runtime.Free;
  end;
end;


procedure TWebModule1.WebModule1DefaultHandlerAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  HTMLSource,
  HTMLPascalScriptEmbedded: string;
begin
  //Set up HTMLSource at your convenience
  HTMLSource:= GetCurrentDir + '\testPage.htm';
  HTMLPascalScriptEmbedded:= LoadStrFromFile(HTMLSource);
  Response.Content:= ProducePage(HTMLPascalScriptEmbedded);
end;


end.

8 comments:

  1. Nice, but it compiles the same page script for every request? I guess the compiled page script should be cached for better performance.

    ReplyDelete
  2. FWIW DWScript supports all the above out of the box, but with a more capable language, faster compiler, faster execution, lower memory usage, more robust runtime, more robust compiler, multi-threading friendliness, and also has a wealth of other capabilities (like being able to compile pascal into javascript for client-side processings).
    Least but not least for a server-side usage, unlike PascalScript, DWScript is also sandboxed by default, meaning secure execution is supported, as well as interrupting/killing any script at any time without leaks.
    There is a FreePascal port in progress if you're interested with that.

    ...alas (and this is my fault) demos for that purpose are kinda limited... :(

    ReplyDelete
  3. Hi Eric, thank you for your comments. DWScript sounds really nice.

    Is this the project page? http://code.google.com/p/dwscript/

    I will be taking a look shortly.

    PS: A Free Pascal port is most welcome.

    ReplyDelete
  4. With Raudus you can create web-applications using Delphi 7..XE2 and Lazarus. You can create desktop-like and mobile applications. Have a look at demos: http://www.raudus.com/samples/

    ReplyDelete
  5. Hello friends,

    PascalScript is very nice. I'll implement this nice feature in Brook framework:

    http://brookframework.org

    Regards,

    Silvio Clécio.

    ps. The Brook description:

    Brook framework is the perfect Free Pascal framework for your web applications. It's pure Pascal. You don't need to leave your preferred programming language.
    It's complete: simple actions or configurable actions for database access, advanced routing features, wizard for Lazarus, support for internationalization, high compatibility with JSON structures, eazy and elegant REST implementation, plugins for varied needs, extensible and decoupled brokers... In fact, you have several reasons to adopt Brook as your web development framework.

    ReplyDelete
  6. Hello buddy,

    What do you think of we implement this feature in Brook framework? It can be called as BSP (Brook Server Pages).

    If you are interested we can open a new issue (https://github.com/silvioprog/brookframework/issues).

    Thank you! (y)

    Silvio Clécio.

    ReplyDelete