Discussion:
Strings and PChar (D4 Pro)
(too old to reply)
P E Schoen
2016-08-27 09:39:10 UTC
Permalink
Raw Message
This is probably a very basic question, but I may have been improperly
casting a string to PChar in some cases. For instance, I got the following
code to work as shown:

====================================================
procedure TfmOrtDatabase.btBackupClick(Sender: TObject);
var udfn: OrtData.TUserDataFile;
S, dbFileName, saveFileName, dirName: String;
// currDir: String;
currDir: array[0..50] of Char;
begin
if Application.MessageBox( 'Backup Database Files?',
'Backup', MB_YESNO ) = ID_YES then begin //1
// currDir := '01234567890123456789012345678901234567890123456789';
// SetLength(currDir,50);
GetCurrentDirectory(sizeof(currDir)+1, @currDir);
dirName := GetSystemFolder(CSIDL_PERSONAL);
dirName := dirName + '\Ortmaster';
SelectDirectory(dirName, [sdAllowCreate,sdPerformCreate], 0);
// SelectDirectory('Select Directory', dirName, dirName);
S := FormatDateTime('yyyymmdd', Now);
fmDBSaveDialog.InitialDir := dirName;
for udfn := udfCust to udfTypes do begin //2
dbFileName := ProgramDataFolder + '\' +
OrtData.UserFileName[udfn]+'.dbf';
fmDBSaveDialog.FileName := OrtData.UserFileName[udfn]+'.dbf';
if not (fmDBSaveDialog.Execute) then exit;
if (FileExists(dbFileName)) then begin //3
saveFileName := fmDBSaveDialog.FileName + '-' + S;
if not( CopyFile( pChar(dbFileName), PChar(saveFileName), FALSE ) )
then
MessageDlg('File Copy Error', mtInformation, [mbOK], 0);;
end; //-3
end; //-2

fmDBSaveDialog.InitialDir := dirName;
dbFileName := 'Ortmaster' + '.rcf';
fmDBSaveDialog.FileName := dbFileName;
dbFileName := ProgramDataFolder + '\Ortmaster' + '.rcf';
if not (fmDBSaveDialog.Execute) then exit;
if (FileExists(dbFileName)) then begin //2
saveFileName := fmDBSaveDialog.FileName + '-' + S;
if not( CopyFile( pChar(dbFileName), pChar(saveFileName), FALSE ) )
then
MessageDlg('File Copy Error', mtInformation, [mbOK], 0);;
end; //-2
SetCurrentDirectory(currDir);
end; //-1
end;

=========================================================

If I used an uninitialized string, the GetCurrentDirectory() function
failed, even when I initialized the string to a constant as shown. It's
rather late, and maybe my brain is fogged, but it seems like there should be
a better way to do this. It would be nice if there were a function like
GetSystemFolder() that returns a string.

BTW, the SelectDirectory() function seems to be what I was searching for in
a post from 7/20/15.

Also, I'm not sure why the current directory got changed by the function.

Thanks,

Paul
P E Schoen
2016-08-27 10:03:01 UTC
Permalink
Raw Message
"P E Schoen" wrote in message news:nprn44$mvb$***@dont-email.me...

I found a simpler function:

===============================================
procedure TfmOrtDatabase.btBackupClick(Sender: TObject);
var currDir: String;

begin
GetDir(0, currDir);
...
ChDir(currDir);
end;
===============================================

But I would still like to learn more about strings and PChar.

Thanks,

Paul
P E Schoen
2016-08-27 10:28:39 UTC
Permalink
Raw Message
Post by P E Schoen
But I would still like to learn more about strings and PChar.
I found the following discussion, which is helpful:

http://www.delphigroups.info/2/0d/17014.html

Thanks,

Paul
JJ
2016-08-27 11:00:09 UTC
Permalink
Raw Message
Post by P E Schoen
But I would still like to learn more about strings and PChar.
Let's start with the simplest one: ShortString. ShortString storage has a
fixed 256 bytes length. This is why ShortString can't hold more than 255
characters. The first byte is the length of the string. The remaining bytes
are the string characters. The data for "ABC" would be like below (in
hexadecimal) where xx is an undefined byte.

03,41,42,43,xx,xx,...

i.e.

ShortStringStorage = record
StringLength : Byte;
StringData : Array [1..255] of Char;
end;

A pointer to a ShortString (e.g. @MyShortString) would point to the first
byte of its storage - to the string length. MyShortString[0] would also
point to the string length - as Char type. And you can modify the string
length by altering the first byte. SizeOf(MyShortString) will always resolve
to 256 if MyShortString was declared as just ShortString. If it was declared
as ShortString[100], SizeOf(MyShortString) would resolve to 101.
Length(MyShortString) will resolve to the string length.


PChar...
PChar type is just a pointer to a Char type. It doesn't hold any actual
characters. It's equivalent to: ^Char. When used as a string pointer, the
data it points to is expected to be a null-terminated string - to the first
character of the string. SizeOf(MyPChar) would resolve to 4 because it's a
pointer. Length() is not applicable for PChar type. The data for "ABC" would
be like below.

41,42,43,00


String...
String type is a combination between ShortString and PChar (null terminated
string). The String storage layout is like ShortString except that the
string length is 16-bit instead of 8-bit. The data for "ABC" would be like
below.

03,00,41,42,43,00

i.e.

(*
StringStorage = record
StringLength : Word;
StringData : Array [1..n] of Char;
end;
Where n is variable. i.e. variable field size
*)

The type itself works similar to PChar (i.e. a pointer), but it points to
the first character of the string instead to the string length. i.e. to the
third byte. SizeOf(MyString) would also resolve to 4 just like PChar.
Length(MyString) would resolve to the string length. You can't use
MyString[0] to access the string length. It's not allowed because the string
length is 16-bit. You'll have to use Length() instead.


Typecasting to PChar...
String can be safely typecasted to PChar, but not a pointer to ShortString
(PShortString). This is because ShortString is not a null terminated string.
If you need to typecast PShortString to PChar, you'll need to have a null
character following the string data (not the string storage). But be sure
the string length plus the null character doesn't exceed 255 characters.
e.g.

procedure Example;
var MyShortString : ShortString;
var MyString : String;
var MyPChar : PChar;
begin
MyShortString := 'ABC';
if Length(MyShortString) < 255 then
begin
(* Append null character without changing string length *)
MyShortString[Length(MyShortString)+1] := #0;
MyPChar := @MyShortString[1];
end
else
begin
MyString := MyShortString;
MyPChar := PChar(MyString);
end;
Application.MessageBox(MyPChar, 'Dialog', MB_OK);
end;
Hans-Peter Diettrich
2016-08-27 11:41:39 UTC
Permalink
Raw Message
Post by JJ
String...
String type is a combination between ShortString and PChar (null terminated
string). The String storage layout is like ShortString except that the
string length is 16-bit instead of 8-bit. The data for "ABC" would be like
below.
03,00,41,42,43,00
i.e.
(*
StringStorage = record
StringLength : Word;
StringData : Array [1..n] of Char;
end;
Where n is variable. i.e. variable field size
*)
This is not true for 32 bit Delphi, which uses something close to
Windows BSTR (OLE compatible). The pointer goes to the first character
(fully compatible PCHAR), and before the characters the 32 bit size and
reference count is stored. See managed data types in OH.

When a ShortString is retyped into a dynamic String, the compiler
usually mocks about many references to str[0], used to set and retrieve
the used length of the ShortString. The Length and SetLength functions
handle both string types, so that they allow to change string types
without further changes to the existing code.

DoDi
JJ
2016-08-27 10:05:22 UTC
Permalink
Raw Message
Post by P E Schoen
This is probably a very basic question, but I may have been improperly
casting a string to PChar in some cases. For instance, I got the following
[snip]
Post by P E Schoen
If I used an uninitialized string, the GetCurrentDirectory() function
failed, even when I initialized the string to a constant as shown. It's
rather late, and maybe my brain is fogged, but it seems like there should be
a better way to do this. It would be nice if there were a function like
GetSystemFolder() that returns a string.
GetCurrentDirectory() arguments are: the buffer pointer, then the buffer
length. Not the buffer length, then the buffer pointer.

And, currDir variable was declarated as "array[0..50] of Char" which means
that it has a storage of 51 bytes plus one byte alignment padding (a total
of 52 bytes). The "sizeof(currDir)+1" expression would translate to "51+1"
which is 52 bytes. You're lucky you've declared currDir variable as an odd
length Char array. If you've declared it as an array with a length of
multiple of 4, there would be no padding following its storage. The data
following its storage might be used by other variable. So, if you use
"sizeof(currDir)+1" you may overwrite one byte which follows the actual
array - a byte outside of the array.
P E Schoen
2016-08-27 10:15:53 UTC
Permalink
Raw Message
"JJ" wrote in message news:1rco3b1rvcu6.1k5cho7qohwwc$***@40tude.net...

[snip]
Post by JJ
GetCurrentDirectory() arguments are: the buffer pointer, then the buffer
length. Not the buffer length, then the buffer pointer.
From the Delphi help:

DWORD GetCurrentDirectory(

DWORD nBufferLength, // size, in characters, of directory buffer
LPTSTR lpBuffer // address of buffer for current directory
);
Post by JJ
And, currDir variable was declarated as "array[0..50] of Char" which means
that it has a storage of 51 bytes plus one byte alignment padding (a total
of 52 bytes). The "sizeof(currDir)+1" expression would translate to "51+1"
which is 52 bytes. You're lucky you've declared currDir variable as an odd
length Char array. If you've declared it as an array with a length of
multiple of 4, there would be no padding following its storage. The data
following its storage might be used by other variable. So, if you use
"sizeof(currDir)+1" you may overwrite one byte which follows the actual
array - a byte outside of the array.
Yes, I probably should have used "sizeof(currDir)-1".

Also, perhaps I could use the return value of the function with a buffer
size of zero. It would return the size needed, and then I could use alloc()
to create the memory space to use. I just don't know what is the "best" way
to handle functions like this, and I may have been using the PChar() cast
improperly.

Thanks for the quick reply!

Paul
JJ
2016-08-27 11:09:56 UTC
Permalink
Raw Message
Post by P E Schoen
Post by JJ
GetCurrentDirectory() arguments are: the buffer pointer, then the buffer
length. Not the buffer length, then the buffer pointer.
DWORD GetCurrentDirectory(
DWORD nBufferLength, // size, in characters, of directory buffer
LPTSTR lpBuffer // address of buffer for current directory
);
Oh, right. My bad.
Post by P E Schoen
Also, perhaps I could use the return value of the function with a buffer
size of zero. It would return the size needed, and then I could use alloc()
to create the memory space to use. I just don't know what is the "best" way
to handle functions like this, and I may have been using the PChar() cast
improperly.
I usually use String type, Length() and SetLength() since I don't want to be
bothered by memory allocation. Delphi runtime will do it automatically. e.g.

SetLength(MyStr, MAX_PATH-1);
SetLength(MyStr, GetCurrentDirectory(MAX_PATH-1, PChar(MyStr)));

Note that if GetCurrentDirectory() fails, calling GetLastError() would not
retrieve the error code for GetCurrentDirectory(), but the API function(s)
used by SetLength().
Hans-Peter Diettrich
2016-08-27 11:12:42 UTC
Permalink
Raw Message
Post by JJ
[snip]
Post by JJ
GetCurrentDirectory() arguments are: the buffer pointer, then the
buffer length. Not the buffer length, then the buffer pointer.
DWORD GetCurrentDirectory(
DWORD nBufferLength, // size, in characters, of directory buffer
LPTSTR lpBuffer // address of buffer for current directory
);
According to MSDN I'd think that the length comes first.
Post by JJ
Also, perhaps I could use the return value of the function with a buffer
size of zero. It would return the size needed, and then I could use
alloc() to create the memory space to use. I just don't know what is the
"best" way to handle functions like this, and I may have been using the
PChar() cast improperly.
Most API functions, dealing with variable amount of data, return the
required size when called with a Nil pointer. MSDN also claims that the
*required* size is returned by GetCurrentDirectory, if the string
doesn't fit into the buffer, but I'd not rely on that. At least the call
succeeded if the returned (actual) size is lower than the allocated size.

All non-empty dynamic Strings are automatically terminated by an
invisible and not counted zero char. I.e. you can/should SetLength(str,
charcount) to extend or shrink a dynamic String to the required length,
with a zero byte at its end as expected by API (C) funtions.

DoDi
P E Schoen
2016-08-27 22:59:38 UTC
Permalink
Raw Message
Post by Hans-Peter Diettrich
Post by P E Schoen
DWORD GetCurrentDirectory(
DWORD nBufferLength, // size, in characters, of directory buffer
LPTSTR lpBuffer // address of buffer for current directory
According to MSDN I'd think that the length comes first.
Yes, as described here:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa364934%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
Post by Hans-Peter Diettrich
Most API functions, dealing with variable amount of data, return the
required size when called with a Nil pointer. MSDN also claims that the
*required* size is returned by GetCurrentDirectory, if the string doesn't
fit into the buffer, but I'd not rely on that. At least the call succeeded
if the returned (actual) size is lower than the allocated size.
All non-empty dynamic Strings are automatically terminated by an invisible
and not counted zero char. I.e. you can/should SetLength(str, charcount)
to extend or shrink a dynamic String to the required length, with a zero
byte at its end as expected by API (C) funtions.
I found another Delphi function that works with strings. I made the
following test project:

procedure TfmTestMain.Button1Click(Sender: TObject);
var curDir, curDir2, curDir3: String;
PcurDir3: PChar;
begin
curDir := GetCurrentDir;
setLength(curDir2, MAX_PATH); //fsDirectory);
GetCurrentDirectory(length(curDir2), PChar(curDir2) );
curDir3 := StrPas(PcurDir3);
setLength(curDir3, MAX_PATH);
PcurDir3 := @curDir3[1];
GetCurrentDirectory(length(curDir3), PcurDir3 );
Label1.Caption := curDir;
Label2.Caption := curDir2;
Label3.Caption := curDir3;
end;

This works, but I get a warning that PcurDir3 might not be initialized. I
think I understand that, since the curDir3 string has not been initialized.
The StrPas function is not needed. I think I am beginning to understand the
string and PChar concepts better.

Thanks,

Paul

Hans-Peter Diettrich
2016-08-27 10:50:09 UTC
Permalink
Raw Message
Post by P E Schoen
This is probably a very basic question, but I may have been improperly
casting a string to PChar in some cases. For instance, I got the
====================================================
procedure TfmOrtDatabase.btBackupClick(Sender: TObject);
var udfn: OrtData.TUserDataFile;
S, dbFileName, saveFileName, dirName: String;
// currDir: String;
currDir: array[0..50] of Char;
begin
if Application.MessageBox( 'Backup Database Files?',
'Backup', MB_YESNO ) = ID_YES then begin //1
// currDir := '01234567890123456789012345678901234567890123456789';
// SetLength(currDir,50);
Unlike a fixed size ShortString, a dynamic string variable is a pointer
to a dynamically allocated char array. I'd guess that sizeof(currDir)
will return 4, the size of a pointer. Try Length instead, which returns
the current (allocated) size of the string. Also use PCHAR(currDir) to
get a pointer to the payload, not to the pointer variable.

DoDi
Loading...