ustring: Dynamic zstring library.

User projects written in or related to FreeBASIC.
StringEpsilon
Posts: 42
Joined: Apr 09, 2015 20:49

ustring: Dynamic zstring library.

Postby StringEpsilon » Dec 16, 2016 15:42

Summary:

ustring is a wrapper UDT for zstrings. It allows you to easily use dynamically sized zstrings, while aiming to be UTF-8 friendly.

For example, all lengths and indices are based on character count, not byte count. Meaning you don't need to worry about stuff like that.

Goals:
* A variable length zstring, that behaves like a normal (z)string in other freebasic code
* Correct character counting and sane string manipulation
* Decent performance.

Non-Goals:
* Rendering of any kind. Use pango for that.
* "Typographical manipulation", i.e. lcase and ucase, ltr- and rtl marks, ...
* 'Sane' support for combining characters.
* Identification / categorization of codepoints and characters.

License:
Mozilla Public License 2.0 (weak copyleft).

Repository:
https://github.com/StringEpsilon/ustring

Feedback appreciated. Especially on performance.
fxm
Posts: 9408
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: ustring: Dynamic zstring library.

Postby fxm » Dec 17, 2016 7:55

Only some remarks by just watching rapidly your code:
- define explicitly all passing parameter modes (for those which compile with option '-w param/pedantic').
- the forward declaration of 'ustring' is not necessary.
- it may be interesting to define the operator '[](byval index as uinteger) as ubyte' returning by reference (to can also do assignment).
- in function 'CountCharacters(byval utf8string as ubyte ptr) as uinteger', it is simplier to start with 'dim charCount as uinteger = -1' rather than to substract '1' at the end.
- in function 'CountCharacters(byval utf8string as ubyte ptr) as uinteger', 'peek' use is not necessary ('codePoint is an Ubyte and 'utf8string' an Ubyte Ptr):
codePoint = peek(ubyte, utf8string)
codePoint = *utf8string
- bug in operator '&(byref value as ustring, byval value2 as zstring ptr) as ustring', the local instance 'sum' is not used but erroneously byref parameter 'value' instead.
- in operator 'ustring.let(byval value as zstring ptr)' and constructor 'ustring(byval value as zstring ptr)', code optimization:
this._bufferSize = (int( this._length / ChunkSize )+2) * ChunkSize
this._bufferSize = (this._length \ ChunkSize + 2) * ChunkSize
- in function 'ustring.CharToByte(byval index as uinteger) as long', 'peek' use is not necessary because 'Ubyte' assignment is compatible with 'Zstring':
codePoint = peek(ubyte, this._buffer + byteIndex)
codePoint = this._buffer[byteIndex]
- in function 'ustring.Instr(byref expression as ustring, byval start as uinteger = 0) as long', 'peek' use is not necessary because 'Ubyte' assignment is compatible with 'Zstring':
codePoint = peek(ubyte, this._buffer+j)
codePoint = this._buffer[j]
- in function 'ustring.Char(byval index as uinteger) as ustring' 'peek' use is not necessary because 'Ubyte' assignment is compatible with 'Zstring':
memcpy(value._buffer, this._buffer + index, GetCodePointLength(peek(ubyte, this._buffer + i)))
memcpy(value._buffer, this._buffer + index, GetCodePointLength(this._buffer[i]))
- in function 'EscapedToUtf8(byval escapedPoint as zstring ptr) as zstring ptr' and function 'EscapedToUtf8(byref escapedPoint as ustring) as zstring ptr', casting 'result' to 'Zstring Ptr' is not necessary because of compatibility of 'Ubyte Ptr' to 'Zstring Ptr':
return cast(zstring ptr,result)
return result

Proposed improved code:

Code: Select all

/'   
   This Source Code Form is subject to the terms of the Mozilla Public
   License, v. 2.0. If a copy of the MPL was not distributed with this
   file, You can obtain one at http://mozilla.org/MPL/2.0/.
   
   (c) 2016 - StringEpsilon.
'/

' TODOs:
' * Function to validate a UTF-8 string
' * 'DeescapeString" or something to actually use EscapedToUtf8()
' * Make EscapedToUtf8() more robust (actually check for \u and u+, etc. )

#include once "crt.bi"

namespace ustringConstants

   dim shared as ubyte charLenghtLookup(256) = _
      { _
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, _
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, _
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, _
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, _
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, _
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, _
         2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, _
         3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 4,4,4,4,4,4,4,4,5,5,5,5,6,6,6,6 _
      }

   #define GetCodepointLength(codepoint) (ustringConstants.charLenghtLookup(codepoint))

   #ifndef NUL
   const NUL = 0
   #endif
   const ChunkSize = 8 ' TODO: Find a good chunk size.
   const replacementChar as string  = "?"

end namespace

function CountCharacters(byval utf8string as ubyte ptr) as uinteger
   dim charCount as uinteger = -1 'to subtract the NUL byte.
   dim codePoint as ubyte
   do
      codePoint = *utf8string
      utf8string += GetCodepointLength(codePoint)
      charCount += 1
   loop until codepoint = 0
   return charCount
end function


using ustringConstants

type ustring
   'private:
      _buffer as zstring ptr
      _length as uinteger       ' The length, in bytes, of the string data.
      _bufferSize as uinteger    ' The current size of the data buffer.
      _characters as uinteger    ' Actual count of characters / glyphs.
   'private:
      declare function CharToByte(byval index as uinteger) as long
   
   public:
      ' Initalizes the ustring with empty data
      declare constructor()
     
      ' Initalizes the ustring with the given zstring value
      declare constructor(byval value as zstring ptr)
     
      ' Initalizes the ustring by making a copy of the given ustring
      declare constructor(byref value as ustring)
     
      declare destructor()
     
      declare operator +=(byref value as zstring)
      declare operator +=(byref value as ustring)
      '~ declare operator +=(byref value as string)
      declare operator cast() byref as zstring
      declare operator [](byval index as uinteger) byref as ubyte
     
      declare operator let(byval value as zstring ptr)     
     
      declare property Size() as uinteger
      declare property Length() as uinteger
     
      ' TODO
      declare function Mid(byval start as uinteger, byval lenght as uinteger) as ustring
     
      declare function Left(byval lenght as uinteger) as ustring
      declare function Right(byval length as uinteger) as ustring
     
      ' TODO
      declare function Instr(byref expression as ustring, byval start as uinteger = 0) as long
      declare function Instr(byref expression as zstring, byval start as uinteger = 0) as long
     
      ' TODO
      declare function InstrRev(byref expression as ustring, byval start as uinteger = 0) as long
      declare function InstrRev(byref expression as zstring, byval start as uinteger = 0) as long
     
      ' TODO
      declare static function space(byval length as uinteger) as ustring
     
      declare function Char(byval index as uinteger) as ustring
   private:
      declare function GetcharCount() as uinteger
end type


declare function EscapedToUtf8 overload (byval escapedPoint as zstring ptr) as zstring ptr
declare function EscapedToUtf8 overload (byref escapedPoint as ustring) as zstring ptr

declare function left overload (byref value as ustring, byval length as uinteger) as ustring
declare function right overload (byref value as ustring, byval length as uinteger) as ustring

operator len(byref value as ustring) as integer
   return value.Length
end operator

operator &(byref value as ustring, byval value2 as zstring ptr) as ustring
   dim sum as ustring = value
   sum += value2
   return sum
end operator

operator ustring.let(byval value as zstring ptr)
   if (len(*value) > 0 ) then
      if (this._buffer <> 0) then
         deallocate(this._buffer)
         
      end if
      this._length = len(*value)
      this._bufferSize = (this._length \ ChunkSize + 2) * ChunkSize
      this._buffer = callocate(this._bufferSize)
      memcpy(this._buffer, value, this._length)
   end if
end operator

destructor ustring()
   deallocate(this._buffer)
end destructor

constructor ustring()
   this._buffer = callocate(ChunkSize)
   this._length = 0
   this._bufferSize = chunkSize
end constructor

constructor ustring(byval value as zstring ptr)
   this._length = len(*value)
   this._bufferSize = (this._length \ ChunkSize + 2) * ChunkSize
   this._buffer = callocate(this._bufferSize)
   memcpy( this._buffer, value, this._length )
end constructor

constructor ustring(byref value as ustring)
   this._length = value._length
   this._bufferSize = value._bufferSize
   this._buffer = allocate(this._bufferSize)
   memcpy( this._buffer, value._buffer, this._bufferSize )
end constructor

operator ustring.+=(byref value as zstring)
   if ( len(value) < this._bufferSize - this._length) then
      memcpy(this._buffer + this._length, @value, len(value))
      this._length += len(value)
   else
      this._buffersize += len(value)
      this._buffer = reallocate(this._buffer, this._buffersize)
      memcpy(this._buffer + this._length, @value, len(value))
      this._length += len(value)
   endif
   this._characters = 0 ' reset the char count
end operator

operator ustring.+=(byref value as ustring)
   if ( value._buffersize < this._bufferSize - this._length) then
      memcpy(this._buffer + this._length, value._buffer, value._length)
      this._length += value._length
   else
      this._buffersize += value._buffersize - chunksize
      this._buffer = reallocate(this._buffer, this._buffersize)
      memcpy(this._buffer + this._length, value._buffer, value._length)
      this._length += value._length
   endif
   this._characters = 0 ' reset the char count   
end operator

operator ustring.[](byval index as uinteger) byref as ubyte
   return this._buffer[index]
end operator

operator ustring.cast() byref as zstring
   return *this._buffer
end operator

property ustring.Length() as uinteger
   if ( this._characters = 0) then
      this._characters = this.GetcharCount()
   end if
   return this._characters
end property

property ustring.Size() as uinteger
   return this._length
end property

function ustring.GetcharCount() as uinteger
   return CountCharacters(this._buffer)
end function

function ustring.CharToByte(byval index as uinteger) as long
   dim codepoint as ubyte
   dim charIndex as uinteger = 0
   dim byteIndex as uinteger = 0
   do
      codePoint = this._buffer[byteIndex]
      if codepoint = 0 then
         return -1
      end if
     
      if index = charIndex then return byteIndex
      charIndex += 1
      byteIndex += GetCodepointLength(codePoint)
   loop until codepoint = 0
end function

function ustring.Instr(byref expression as ustring, byval start as uinteger = 0) as long
   'Error case first:
   dim as uinteger index = this.CharToByte(start)
   if (index > this._length) then return -1
   
   dim as uinteger expressionLength = expression._length
   dim as uinteger i = index
   do
      if i > this._length - expressionLength then
         exit do
      end if
      if memcmp(this._buffer + i, expression._buffer, expressionLength) = 0 then
         dim charCount as uinteger
         dim codePoint as ubyte
         dim j as uinteger
         do
            codePoint = this._buffer[j]
            if codepoint = 0 then
               return -1
            end if
            if j > i then return -1
            if j = i then return charCount
            charCount += 1
            j += GetCodepointLength(codePoint)
         loop until codepoint = 0 OR j >= i
         return charCount
      end if
      i += 1
   loop
   return -1
end function

function ustring.Char(byval index as uinteger) as ustring
   dim as ustring value
   dim i as long =  this.CharToByte(index)
   if (i <> -1) then
      memcpy(value._buffer, this._buffer + index, GetCodePointLength(this._buffer[i]))
   end if
   return value
end function


function EscapedToUtf8(byval escapedPoint as zstring ptr) as zstring ptr
   dim as ulong codePoint = valulng("&h" & right(*escapedPoint, len(*escapedPoint)-2))   
   dim result as ubyte ptr
   
   if codePoint <= &h7F then
      result = allocate(1)
      result[0] = codePoint
      return result
   endif
   
   if    (&hD800 <= codepoint AND codepoint <= &hDFFF) OR _
      (codepoint > &h10FFFD) then
      return strptr(ustringConstants.replacementChar)
   end if
   
   if (codepoint <= &h7FF) then
      result = allocate(2)
      result[0] = &hC0 OR (codepoint SHR 6) AND &h1F
      result[1] = &h80 OR codepoint AND &h3F
      return result
   end if
   if (codepoint <= &hFFFF) then
      result = allocate(3)
        result[0] = &hE0 OR codepoint SHR 12 AND &hF
        result[1] = &h80 OR codepoint SHR 6 AND &h3F
        result[2] = &h80 OR codepoint AND &h3F
        return result
    end if
   
   result = allocate(4)
   result[0] = &hF0 OR codepoint SHR 18 AND &h7
   result[1] = &h80 OR codepoint SHR 12 AND &h3F
   result[2] = &h80 OR codepoint SHR 6 AND &h3F
   result[3] = &h80 OR codepoint AND &h3F
   
   return result
end function

function EscapedToUtf8(byref escapedPoint as ustring) as zstring ptr
   dim as ulong codePoint = valulng("&h" & right(escapedPoint, len(escapedPoint)-2))   
   dim result as ubyte ptr
   
   if codePoint <= &h7F then
      result = allocate(1)
      result[0] = codePoint
      return result
   endif
   
   if    (&hD800 <= codepoint AND codepoint <= &hDFFF) OR _
      (codepoint > &h10FFFD) then
      return strptr(ustringConstants.replacementChar)
   end if
   
   if (codepoint <= &h7FF) then
      result = allocate(2)
      result[0] = &hC0 OR (codepoint SHR 6) AND &h1F
      result[1] = &h80 OR codepoint AND &h3F
      return result
   end if
   if (codepoint <= &hFFFF) then
      result = allocate(3)
        result[0] = &hE0 OR codepoint SHR 12 AND &hF
        result[1] = &h80 OR codepoint SHR 6 AND &h3F
        result[2] = &h80 OR codepoint AND &h3F
        return result
    end if
   
   result = allocate(4)
   result[0] = &hF0 OR codepoint SHR 18 AND &h7
   result[1] = &h80 OR codepoint SHR 12 AND &h3F
   result[2] = &h80 OR codepoint SHR 6 AND &h3F
   result[3] = &h80 OR codepoint AND &h3F
   
   return result
end function

function left overload (byref value as ustring, byval length as uinteger) as ustring
   return left(*value._buffer, value.CharToByte(length))
end function

function right overload (byref value as ustring, byval length as uinteger) as ustring
   return *(value._buffer + value.CharToByte(len(value)-length) )
end function
fxm
Posts: 9408
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: ustring: Dynamic zstring library.

Postby fxm » Dec 17, 2016 10:41

Check also everywhere your code because it seems to me that '(*this._buffer)[this._length]' is not always set to '0' (its memory is allocated thanks to ChunkSize, but not always set to 0):
That is mandatory for the 'Cast' operator returning a 'Zstring' by reference (return *this._buffer).
Check at least at:
- operator 'ustring.+=(byref value as zstring)'
- operator 'ustring.+=(byref value as ustring)'
Last edited by fxm on Dec 17, 2016 12:51, edited 1 time in total.
StringEpsilon
Posts: 42
Joined: Apr 09, 2015 20:49

Re: ustring: Dynamic zstring library.

Postby StringEpsilon » Dec 17, 2016 11:21

Thanks for the feedback FXM. I used most of it. But this one: "it may be interesting to define the operator '[](byval index as uinteger) as ubyte' returning by reference (to can also do assignment)." I disagree with. In fact, that operator should return a ustring. I don't know why it was even in the code like that to begin with (probably debugging).

To demonstrate the problem:

Code: Select all

dim as ustring test = "I ♥ Freebasic"
test[3] = asc("<")
test[4] = asc("3")


Would result in "I �<3 Freebasic" (as the ♥ is three bytes long). With UTF-8, you should not think of bytes as characters, as it could get messy.

About the cast operator: You mean the non-"NUL" data at the end of the buffer causes trouble? Is that also true if there is a 0-byte where the string inside the buffer should end?
fxm
Posts: 9408
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: ustring: Dynamic zstring library.

Postby fxm » Dec 17, 2016 11:54

A Zstring cannot contain 'Chr(0)' as usefull character because that is the reserved termination character (a Zstring has not descriptor containing the string length as for a var-len String):

Code: Select all

Dim As Zstring * 10 z = "FreeBASIC"  ' allocated len("FreeBASIC")+1 characters
Print "'" & z & "'", Len(z)
z[4] = 0
Print "'" & z & "'", Len(z)

Sleep
fxm
Posts: 9408
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: ustring: Dynamic zstring library.

Postby fxm » Dec 17, 2016 12:22

Example of bug (termination character not set to 0):

Code: Select all

dim as ustring u = "Free"
u += "Basic version 1.06.0 (current version)"
print "'" & u & "'"

dim as ustring u2 = "."
u += u2
print "'" & u & "'"

sleep

Code: Select all

'FreeBasic version 1.06.0 (current version)ÊËÈıÍÎÏ┘┌█▄¦Ì▀ÖgXVòç'
'FreeBasic version 1.06.0 (current version).ËÈıÍÎÏ┘┌█▄¦'

Minimum fix (principle IMHO):

Code: Select all

operator ustring.+=(byref value as zstring)
   if ( len(value) < this._bufferSize - this._length) then
      memcpy(this._buffer + this._length, @value, len(value))
      this._length += len(value)
   else
      this._buffersize += len(value)
      this._buffer = reallocate(this._buffer, this._buffersize)
      memcpy(this._buffer + this._length, @value, len(value)+1)
      this._length += len(value)
   endif
   this._characters = 0 ' reset the char count
end operator

operator ustring.+=(byref value as ustring)
   if ( value._buffersize < this._bufferSize - this._length) then
      memcpy(this._buffer + this._length, value._buffer, value._length)
      this._length += value._length
   else
      this._buffersize += value._buffersize - chunksize
      this._buffer = reallocate(this._buffer, this._buffersize)
      memcpy(this._buffer + this._length, value._buffer, value._length+1)
      this._length += value._length
   endif
   this._characters = 0 ' reset the char count   
end operator
StringEpsilon
Posts: 42
Joined: Apr 09, 2015 20:49

Re: ustring: Dynamic zstring library.

Postby StringEpsilon » Dec 17, 2016 15:02

Thanks for the report and patch.

You misunderstood my previous question. I know that 0 is the terminator.

String data in memory, without your patch

Code: Select all

46 72 65 65 42 41 53 49 43 99 F0 AA ...


And the garbled data begins at 99. My idea was to simply set the last byte (this._length +1) to 00:

Code: Select all

this._buffer[this._length+1] = 0
 ->
46 72 65 65 42 41 53 49 43 _00_ F0 AA ...


To always make sure the string is properly terminated.
caseih
Posts: 1405
Joined: Feb 26, 2007 5:32

Re: ustring: Dynamic zstring library.

Postby caseih » Dec 17, 2016 15:35

Just curious why you are using a zstring buffer when a normal dynamic string would work as well, and you could use the existing dynamic properties of it (automatic allocation of memory, expanding when necessary, etc). Also a dynamic string will cast to zstring easily, and has an implicit null terminator at the end of the string. Under the hood a dynamic string usually is a zstring buffer.

That said, dynamic strings have a long-time problem or caveat (bug in my opinion) where a zero-length string or "" casts to a NULL pointer zstring. In my mind a dynamic string that is empty should always consist of a real buffer with a single null terminator in it. Fortunately in your case you could work around that problem when casting to a zstring.
StringEpsilon
Posts: 42
Joined: Apr 09, 2015 20:49

Re: ustring: Dynamic zstring library.

Postby StringEpsilon » Dec 17, 2016 17:05

@caseih

That... that is a really good question. For some reason I had in my head that when doing Unicode, I should use a zstring and went with that without thinking twice about it. I guess I can safe myself from some memory management by using normal strings.
fxm
Posts: 9408
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: ustring: Dynamic zstring library.

Postby fxm » Dec 17, 2016 23:01

caseih wrote:That said, dynamic strings have a long-time problem or caveat (bug in my opinion) where a zero-length string or "" casts to a NULL pointer zstring. In my mind a dynamic string that is empty should always consist of a real buffer with a single null terminator in it.

As these strptr() and str() functions revised for returning respectively Zstring Ptr and Zstring:

Code: Select all

Function Zstrptr (Byref s As String) As Zstring Ptr
  If Strptr(s) = 0 Then
    Return @""
  Else
    Return Strptr(s)
  End If
End Function

Function Zstr (Byref s As String) Byref As Zstring
  If Strptr(s) = 0 Then
    Return ""
  Else
    Return *Strptr(s)
  End If
End Function

Dim As String s
Dim As Zstring Ptr pz
Dim Byref As Zstring rz = *Cptr(Zstring Ptr, 0)

s = "FreeBASIC"

pz = Zstrptr(s)
Print pz,                   ' zstring pointer
Print Len(*pz),             ' zstring length
Print """" & (*pz) & """",  ' "zstring"
Print (*pz)[Len(*pz)]       ' terminal ubyte

@rz = @(Zstr(s))
Print @rz,                   ' zstring pointer
Print Len(rz),               ' zstring length
Print """" & (rz) & """",    ' "zstring"
Print rz[Len(rz)]            ' terminal ubyte

Print

s = ""

pz = Zstrptr(s)
Print pz,                   ' zstring pointer
Print Len(*pz),             ' zstring length
Print """" & (*pz) & """",  ' "zstring"
Print (*pz)[Len(*pz)]       ' terminal ubyte

@rz = @(Zstr(s))
Print @rz,                   ' zstring pointer
Print Len(rz),               ' zstring length
Print """" & (rz) & """",    ' "zstring"
Print rz[Len(rz)]            ' terminal ubyte

Sleep
    The literal "" defines a null constant Zstring in memory (only the terminal ubyte=0 is reserved in memory).
PaulSquires
Posts: 795
Joined: Jul 14, 2005 23:41
Contact:

Re: ustring: Dynamic zstring library.

Postby PaulSquires » Dec 18, 2016 18:47

fxm wrote:

Code: Select all

Function Zstrptr (Byref s As String) As Zstring Ptr
  If Strptr(s) = 0 Then
    Return @""
  Else
    Return Strptr(s)
  End If
End Function


I am curious. The Return @"" returns the address of the "" empty string to the ZString Ptr, but when the function ends doesn't the address of the created "" become undefined because the "" is local to the function? When the operating system eventually deallocates the "" and you later try to dereference the pointer it could actually then point to invalid memory?
fxm
Posts: 9408
Joined: Apr 22, 2009 12:46
Location: Paris suburbs, FRANCE

Re: ustring: Dynamic zstring library.

Postby fxm » Dec 18, 2016 19:28

The literal strings are treated as 'ZString * size' datatype (and not as 'var-len string' datatype).
The memory for literal strings is always allocated in the .DATA section of the executable, as for the static variables if they are initialized when defined.
This allows the element data to persist throughout program execution.

Code: Select all

Dim As Zstring Ptr pz

Scope
  pz = @"FreeBASIC"
End Scope

Print *pz

Sleep
One can check the asm code after compilation:

Code: Select all

   .intel_syntax noprefix

.section .text
.balign 16
_fb_ctor__FBIDETEMP:
push ebp
mov ebp, esp
sub esp, 4
.Lt_0002:
mov dword ptr [ebp-4], 0
mov eax, offset _Lt_0004
mov dword ptr [ebp-4], eax
push 1
push dword ptr [ebp-4]
call _fb_StrAllocTempDescZ@4
push eax
push 0
call _fb_PrintString@12
push -1
call _fb_Sleep@4
.Lt_0003:
mov esp, ebp
pop ebp
ret

.section .data
.balign 4
_Lt_0004:   .ascii   "FreeBASIC\0"

.section .ctors
.int _fb_ctor__FBIDETEMP
PaulSquires
Posts: 795
Joined: Jul 14, 2005 23:41
Contact:

Re: ustring: Dynamic zstring library.

Postby PaulSquires » Dec 18, 2016 21:28

Thanks fxm, excellent explanation and answer to my question.

Return to “Projects”

Who is online

Users browsing this forum: No registered users and 1 guest