External routines in DWS

I’ve been working with Eric Grange on adding a new feature to the DWS compiler recently: external routines.  The goal of this feature is to allow DWS code to call into native routines like Delphi code can call into routines in a DLL by writing a function signature and marking it external, without having to use a TdwsUnit component and create a bunch of heavy-overhead binding code.

This is accomplished by using a simple JIT that takes a DWS function object as its input, and outputs a little stub of machine code that does the same thing as the binding routine you would have to write otherwise.

The eventual goal is to be able to call out to DLLs as well as internal native routines with this.  That will require supporting multiple calling conventions, and since handling the function call is the whole point of the JIT, that will probably mean creating multiple JITs.  But at the moment, I’m still working on the first one, to support the register calling convention.

It’s nowhere near “production-ready” yet, but I’ve got some progress checked in.  So far, it’s able to create stubs for procedures with up to 3 parameters of type Integer or String, with no return value.  You create a header in your DWS script and mark it with the external; directive (no name, just the keyword) and then, after compiling the script but before running it, call the new RegisterExternalFunction method on the compiler to link the external; routine with the procedure it should execute.

My basic methodology has gone like this:

  • Open a Console project in Delphi
  • Create a procedure that looks like a DWS external eval handler
  • Create a procedure with the right signature for the eval handler to call into
  • Write the code for the eval handler
  • Build, put a breakpoint in the eval handler, and run
  • Examine the generated ASM code in the CPU view
  • Figure out what it’s doing
  • Set up my JIT to do the same thing

The only really tricky part so far has been setting up the implicit try/finally block to clean up strings, mostly because there’s a lot of “magic” going on in there and I don’t know what it all does.  I’m sure Barry Kelly the current compiler guy for the Delphi team could explain it in detail, but for the moment I’m just content with doing the same thing the Delphi compiler is doing and making sure it works the same way.

Next on the list will be the other two basic types in DWS, boolean and float.  After that, I’m not sure.  Probably return values, and sets because those are implemented under the hood as integers.

Anyway, like I said this is still a work in progress, but the progress I have so far is checked in, so have a look if anyone’s interested.  As always, feedback is welcome. :)

6 Comments

  1. A. Bouchez says:

    Nice idea.

    About calling convention in Delphi and management of types (including strings), the reference is http://docwiki.embarcadero.com/RADStudio/XE5/en/Program_Control
    But this official wiki page does not document Win64. EMB’s documentation is somewhat broken… :(

    If you want to have some reference code, you can take a look at the code we wrote for our mORMot framework.
    See function TServiceMethod.InternalExecute() in mORMot.pas.
    It handles almost all kind of parameters, on both Win32 and Win64 platforms.
    We use a small asm stub – see procedure CallMethod(var Args: TCallMethodArgs) – and not a JIT here, and all the logic is made before calling the stub.

    Of course, you have some similar code in unit System.Rtti, but we found it to be somewhat difficult to read, and there was some issues (e.g. AFAIR there is an issue when you want to return a string from a function result in Win64).

    All this is about methods execution, but a method is a function with a first “self: TObject” parameter. :)

    • Mason Wheeler says:

      Yeah, doing it the RTTI.pas style was the other option. I discussed both with Eric, and decided to go the JIT route because in the end, it’s a lot more efficient. Essentially, you can either calculate how to map script values to function calls once, or figure it out every time you make an external call. So I figured doing it once was the better option.

    • David Heffernan says:

      There’s not much to say about Win64. The platform defines the ABI. All that needs to be said is the handling of return values (var parameters unless they fit in registers), but that’s just the same as Win32.

      • A. Bouchez says:

        @David
        Yes, the ABI is the ABI, but in some cases, EMB did not follow it, when dealing with Delphi “private” types.
        For instance, a function returning a string should return it as a last parameter transmitted as var.
        This is the case in Win32, as stated by the official doc.

        But in Win64, this is not the case: it is transmitted as 1st parameter, AFAIR.
        This is not a big issue, since a Delphi string should only be used within Delphi (or C++).
        But this is an issue when you want to do something like Mason wants.
        I had to circumvent this unexpected implementation detail, and Mason should be aware of it, when targetting Win64 (which is not yet the case for DWS JIT).

        AFAIR the System.RTTI.pas was broken about it (at least with Delphi XE2).
        All this appeared with mORMot unit testing code.
        What is the test coverage of RTTI.pas under Win64 at EMB’s?

  2. EMB says:

    Hey! I don’t have nothing with the documentation. :P

    I’m still learning DWS yet. I didn’t find a way I could use it for my current softwares yet. But is something I always follow close… well, a few weeks later.

    Thanks for your work.

  3. […] been working on the stub-building JIT for external routines in DWS lately, and I just checked in a bunch of updates.  The JIT will currently handle parameters of […]

Leave a Reply