Victor Melo
Victor Melo

Reputation: 310

Delphi - Write a .pas library with functions

I'm writing some functions in Delphi using Assembly. So I want to put it in a .pas file called Strings.pas. To use in uses of a new Delphi software. What do I need to write, to make it a valid library?
My function is like this:

function Strlen(texto : string) : integer;
begin
  asm
    mov esi, texto
    xor ecx,ecx
    cld
    @here:
    inc ecx
    lodsb
    cmp al,0
    jne @here
    dec ecx
    mov Result,ecx
  end;
end;

That counts the numbers of chars in the string. How can I make it in a lib Strings.pas to call with uses Strings; in my form?

Upvotes: 2

Views: 1807

Answers (3)

Paolo Fassin
Paolo Fassin

Reputation: 365

The my two solutions to get the length of two types of string, as for says Peter Cordes are not both useful. Only the "PAnsiCharLen()" could be an alternative solution, but not as fast as it is StrLen() (optimized) of Amaud Bouchez, that it is about 3 times faster than mine.

10/14/2017 (mm/dd/yyy): Added one new function (Clean_Str).

However, for now, I propose three small corrections to both of them (two suggested by Peter Cordes: 1) use MovZX instead of Mov && And; 2) Use SetZ/SetE instead LAHF/ShL, use XOr EAX,EAX instead XOr AL,AL); in the future I could define the functions in assembly (now they are defined in Pascal):

unit MyStr;

{ Some strings' function }

interface

Function PAnsiCharLen(S:PAnsiChar):Integer;

{ Get the length of the PAnsiChar^ string. }

Function ShortStrLen(S:ShortString):Integer;

{ Get the length of the ShortString^ string. }

Procedure Clean_Str(Str:ShortString;Max_Len:Integer);

{ This function can be used to clear the unused space of a short string
  without modifying his useful content (for example, if you save a
  short-string field in a file, at parity of content the file may be
  different, because the unused space is not initialized).

  Clears a String Str_Ptr ^: String [], which has
  Max_Len = SizeOf (String []) - 1  characters, placing # 0
  all characters beyond the position of Str_Ptr ^ [Str_Ptr ^ [0]] }

implementation

Function PAnsiCharLen(S:PAnsiChar):Integer;

{ EAX EDX ECX are 1°, 2° AND 3° PARAMETERs.
  Can freely modify the EAX, ECX, AND EDX REGISTERs. }

Asm

     ClD             {Clear string direction flag}

     Push  EDI       {Save EDI's reg. into the STACK}

     Mov   EDI,S     {Load S into EDI's reg.}
     XOr   EAX,EAX   {Set AL's reg. with null terminator}
     Mov   ECX,-1    {Set ECX's reg. with maximum length of the string}

     RepNE ScaSB     {Search null and decrease ECX's reg.}

     SetE  AL        {AL is set with FZero}

     Add   EAX,ECX   {EAX= maximum_length_of_the_string - real_length_of_the_string}
     Not   EAX       {EAX= real_length_of_the_string}

     Pop   EDI       {Restore EDI's reg. from the STACK}

End;

Function ShortStrLen(S:ShortString):Integer; Assembler;

{ EAX EDX ECX are 1°, 2° AND 3° PARAMETERs.
  Can freely modify the EAX, ECX, AND EDX REGISTERs. }

Asm

     MovZX EAX,Byte Ptr [EAX]  {Load the length of S^ into EAX's reg. (function's result)}

End;

Procedure Clean_Str(Str:ShortString;Max_Len:Integer); Assembler;

(* EAX EDX ECX are 1°, 2° AND 3° PARAMETERs.
   Can freely modify the EAX, ECX, AND EDX REGISTERs. *)

Asm

     ClD                       {Clear string direction flag}

     Push   EDI                {Save EDI's reg. into the STACK}

     Mov    EDI,Str            {Load input string pointer into EDI's reg.}
     Mov    ECX,Max_Len        {Load allocated string length into ECX's reg.}
     MovZX  EDX,Byte Ptr [EDI] {Load real string length into EDX's reg.}
     StC                       {Process the address of unused space of Str; ...}
     AdC    EDI,EDX            {...  skip first byte and useful Str space}

     Cmp    EDX,ECX            {If EDX>ECX ...}
     CMovGE EDX,ECX            {... set EDX with ECX}
     Sub    ECX,EDX            {ECX contains the size of unused space of Str}

     XOr    EAX,EAX            {Clear accumulator}

     Rep    StoSB              {Fill with 0 the unused space of Str}

     Pop    EDI                {Restore EDI's reg. from the STACK}

End;

end.

Old (incomplete) answer:

"Some new string's functions, not presents in Delphi library, could be these:"

Type Whole=Set Of Char;

Procedure AsmKeepField       (PStrIn,PStrOut:Pointer;FieldPos:Byte;
                              All:Boolean);

{ Given "field" as a sequence of characters that does not contain spaces
  or tabs (# 32, # 9), it takes FieldPos (1..N) field
  to PStrIn ^ (STRING) and copies it to PStrOut ^ (STRING).

  If All = TRUE, it also takes all subsequent fields }

Function  AsmUpCComp         (PStr1,PStr2:Pointer):Boolean;

{ Compare a string PStr1 ^ (STRING) with a string PStr2 ^ (STRING),
  considering the PStr1 alphabetic characters ^ always SHIFT }

Function  UpCaseStrComp      (Str1,Str2:String;Mode:Boolean):ShortInt;

{ Returns: -1 if Str1 < Str2.
            0 is Str1 = Str2.
            1 is Str1 > Str2.

  MODE = FALSE means "case sensitive comparison" (the letters are
  consider them as they are).

  MODE = TRUE means that the comparison is done by considering
  both strings as if they were all uppercase }

Function  KeepLS             (Str:String;CntX:Byte):String;

{ RETURN THE PART OF STR THAT INCLUDES THE FIRST CHARACTER
  OF STR AND ALL THE FOLLOW UP TO THE POSITION CntX (0 to N-1) INCLUDED }

Function  KeepRS             (Str:String;CntX,CsMode:Byte):String;

{ RETURN THE PART OF STR STARTING TO POSITION CntX + 1 (0 to N-1)
  UP TO END OF STR.
  IF CsMode = 0 (INSERT MODE), IF CsMode = 1 (OVERWRITE-MODE):
  IN THIS CASE, THE CHARACTER TO CntX + 1 POSITION IS NOT INCLUDED }

Function  GetSubStr          (Str:String;
                              Pos,Qnt:Byte;CH:Char):String;

{ RETURN Qnt STR CHARACTERS FROM POSITION Pos (1 to N) OF STR;
  IF EFFECTIVE LENGTH IS LESS THAN Qnt, WILL ADDED CHARACTER = CH }

Function  Keep_Right_Path_Str_W(PathName:String;FieldWidth:Byte;
                                FormatWithSpaces:Boolean):String;

{ RESIZE A STRING OF A FILE PATH, FROM PathName;
  THE NEW STRING WILL HAVE A MAXIMUM LENGTH OF FieldWidth CHARACTERS.
  REPLACE EXCEDENT CHARACTERS WITH 3 POINTS,
  INSERTED AFTER DRIVE AND ROOT.
  REPLACE SOME DIRECTORY WITH 3 POINTS,
  ONLY WHEN IT IS NECESSARY, POSSIBLE FROM SECOND.
  FORMAT RETURN WITH SPACE ONLY IF FormatWithSpaces = TRUE }

Function  KeepBarStr         (Percentage,Qnt:Byte;
                              Ch1,Ch2,Ch3:Char):String;

{ THIS IS A FUNCTION WICH MAKES A STRING WICH CONTAINS A REPRESENTATION OF STATE
  OF ADVANCEMENT OF A PROCESS; IT RETURNS A CHARACTERS' SEQUENCE, CONSTITUTED BY  "<Ch1>"
  (LENGTH = Percentage / 100 * Qnt), WITH AN APPROXIMATION OF THE LAST CHARACTER TO
  "<Ch2>" (IF "Percentage / 100 * Qnt" HAS HIS FRACTIONAL'S PART GREATER THAN 0.5),
  FOLLOWED BY AN OTHER CHARACTERS' SEQUENCE, CONSTITUTED BY "<Ch3>" (LENGTH = (100 -
  Percentage) / 100 * Qnt). }

Function  Str2ChWhole        (Str:String;Var StrIndex:Byte;
                              Var ChSet:Whole;
                              Mode:Boolean):Boolean;

{ CONVERT A PART OF Str, POINTED BY StrIndex, IN A ChSet CHARACTER SET;
  IF Mode = TRUE, "StrIn" SHOULD CONTAIN ASCII CODES
  OF CORRESPONDING CHARACTERS EXPRESSED IN DECIMAL SIZE;
  OTHERWISE IT SHOULD CONTAIN CORRESPONDING CHARACTER SYMBOLS }

Function  ChWhole2Str        (ChSet:Whole;Mode:Boolean):String;

{ CONVERT A SET OF CHARACTERS IN A CORRESPONDING STRING;
  IF Mode = TRUE ELEMENTS OF ChSet WILL BE CONVERTED IN ASCII CODES
  EXPRESSED IN DECIMAL SIZE; OTHERWISE THE CORRESPONDING SYMBOLS
  WILL BE RETURNED }

Function  ConverteFSize      (FSize:LongInt;
                              Var SizeStr:TSizeStr):Integer;

{ MAKES THE CONVERSION OF THE DIMENSION OF A FILE IN A TEXT,
  LARGE TO MAXIMUM 5 CHARACTERS, AND RETURN THE COLOR OF THIS STRING }

Function  UpCasePos          (SubStr,Str:String):Byte;

{ Like the Pos () system function, but not "case sensitive" }

Upvotes: 2

Arnaud Bouchez
Arnaud Bouchez

Reputation: 43023

The correct asm version may be:

unit MyStrings; // do not overlap Strings.pas unit

interface

function StringLen(const texto : string) : integer;

implementation

function StringLen(const texto : string) : integer;
asm
    test eax,eax
    jz @done
    mov eax,dword ptr [eax-4]
@done:
end;

end.

Note that:

  • I used MyStrings as unit name, since it is a very bad idea to overlap the official RTL unit names, like Strings.pas;
  • I wrote (const texto: string) instead of (texto: string), to avoid a reference count change at calling;
  • Delphi string type already has its length stored as integer just before the character memory buffer;
  • In Delphi asm calling conventions, the input parameters are set in eax edx ecx registers, and the integer result of a function is the eax register - see this reference article - for Win32 only;
  • I tested for texto to be nil (eax=0), which stands for a void '' string;
  • This would work only under Win32 - asm code under Win64 would be diverse;
  • Built-in length() function would be faster than an asm sub-function, since it is inlined in new versions of Delphi;
  • Be aware of potential name collisions: there is already a well known StrLen() function, which expects a PChar as input parameter - so I renamed your function as StringLen().

Since you want to learn asm, here are some reference implementation of this function.

A fast PChar oriented version may be :

function StrLen(S: PAnsiChar): integer;
asm
    test eax,eax
    mov edx,eax
    jz @0
    xor eax,eax
@s: cmp byte ptr [eax+edx+0],0; je @0
    cmp byte ptr [eax+edx+1],0; je @1
    cmp byte ptr [eax+edx+2],0; je @2
    cmp byte ptr [eax+edx+3],0; je @3
    add eax,4
    jmp @s
@1: inc eax
@0: ret
@2: add eax,2; ret
@3: add eax,3
end;

A more optimized version:

function StrLen(S: PAnsiChar): integer;
// pure x86 function (if SSE2 not available) - faster than SysUtils' version
asm
     test eax,eax
     jz @@z
     cmp   byte ptr [eax+0],0; je @@0
     cmp   byte ptr [eax+1],0; je @@1
     cmp   byte ptr [eax+2],0; je @@2
     cmp   byte ptr [eax+3],0; je @@3
     push  eax
     and   eax,-4              { DWORD Align Reads }
@@Loop:
     add   eax,4
     mov   edx,[eax]           { 4 Chars per Loop }
     lea   ecx,[edx-$01010101]
     not   edx
     and   edx,ecx
     and   edx,$80808080       { Set Byte to $80 at each #0 Position }
     jz    @@Loop              { Loop until any #0 Found }
@@SetResult:
     pop   ecx
     bsf   edx,edx             { Find First #0 Position }
     shr   edx,3               { Byte Offset of First #0 }
     add   eax,edx             { Address of First #0 }
     sub   eax,ecx             { Returns Length }
@@z: ret
@@0: xor eax,eax; ret
@@1: mov eax,1;   ret
@@2: mov eax,2;   ret
@@3: mov eax,3
end;

An SSE2 optimized version:

function StrLen(S: PAnsiChar): integer;
asm // from GPL strlen32.asm by Agner Fog - www.agner.org/optimize
        or       eax,eax
        mov      ecx,eax             // copy pointer
        jz       @null               // returns 0 if S=nil
        push     eax                 // save start address
        pxor     xmm0,xmm0           // set to zero
        and      ecx,0FH             // lower 4 bits indicate misalignment
        and      eax,-10H            // align pointer by 16
        movdqa   xmm1,[eax]          // read from nearest preceding boundary
        pcmpeqb  xmm1,xmm0           // compare 16 bytes with zero
        pmovmskb edx,xmm1            // get one bit for each byte result
        shr      edx,cl              // shift out false bits
        shl      edx,cl              // shift back again
        bsf      edx,edx             // find first 1-bit
        jnz      @A200               // found
        // Main loop, search 16 bytes at a time
@A100:  add      eax,10H             // increment pointer by 16
        movdqa   xmm1,[eax]          // read 16 bytes aligned
        pcmpeqb  xmm1,xmm0           // compare 16 bytes with zero
        pmovmskb edx,xmm1            // get one bit for each byte result
        bsf      edx,edx             // find first 1-bit
        // (moving the bsf out of the loop and using test here would be faster
        // for long strings on old processors, but we are assuming that most
        // strings are short, and newer processors have higher priority)
        jz       @A100               // loop if not found
@A200:  // Zero-byte found. Compute string length
        pop      ecx                 // restore start address
        sub      eax,ecx             // subtract start address
        add      eax,edx             // add byte index
@null:
end;

Or even a SSE4.2 optimized version:

function StrLen(S: PAnsiChar): integer;
asm // warning: may read up to 15 bytes beyond the string itself
        or        eax,eax
        mov       edx,eax             // copy pointer
        jz        @null               // returns 0 if S=nil
        xor       eax,eax
        pxor      xmm0,xmm0
        {$ifdef HASAESNI}
        pcmpistri xmm0,dqword [edx],EQUAL_EACH  // comparison result in ecx
        {$else}
        db $66,$0F,$3A,$63,$02,EQUAL_EACH
        {$endif}
        jnz       @loop
        mov       eax,ecx
@null:  ret
@loop:  add       eax,16
        {$ifdef HASAESNI}
        pcmpistri xmm0,dqword [edx+eax],EQUAL_EACH  // comparison result in ecx
        {$else}
        db $66,$0F,$3A,$63,$04,$10,EQUAL_EACH
        {$endif}
        jnz       @loop
@ok:    add       eax,ecx
end;

You will find all those functions, including Win64 versions, in our very optimized SynCommons.pas unit, which is shared by almost all our Open Source projects.

Upvotes: 2

Remy Lebeau
Remy Lebeau

Reputation: 595897

A .pas file is a unit, not a library. A .pas file needs to have unit, interface, and implementation statements, eg:

Strings.pas:

unit Strings;

interface

function Strlen(texto : string) : integer;

implementation

function Strlen(texto : string) : integer;
asm
  // your assembly code...
  // See Note below...
end;

end.

Then you can add the .pas file to your other projects and use the Strings unit as needed. It will be compiled directly into each executable. You don't need to make a separate library out of it. But if you want to, you can. Create a separate Library (DLL) or Package (BPL) project, add your .pas file to it, and compile it into an executable file that you can then reference in your other projects.

In the case of a DLL library, you will not be able to use the Strings unit directly. You will have to export your function(s) from the library (and string is not a safe data type to pass over a DLL boundary between modules), eg:

Mylib.dpr:

library Mylib;

uses
  Strings;

exports
  Strings.Strlen;

begin
end.

And then you can have your other projects declare the function(s) using external clause(s) that reference the DLL file, eg:

function Strlen(texto : PChar) : integer; external 'Mylib.dll';

In this case, you can make a wrapper .pas file that declares the functions to import, add that unit to your other projects and use it as needed, eg:

StringsLib.pas:

unit StringsLib;

interface

function Strlen(texto : PChar) : integer;

implementation

function Strlen; external 'Mylib.dll';

end.

In the case of a Package, you can use the Strings units directly. Simply add a reference to the package's .bpi in your other project's Requires list in the Project Manager, and then use the unit as needed. In this case, string is safe to pass around.

Note: in the assembly code you showed, for the function to not cause an access violation, you need to save and restore the ESI register. See the section on Register saving conventions in the Delphi documentation.

Upvotes: 14

Related Questions