Unspecified error when calling Word CentimetersToPoints via OLE - delphi

The following code fails with an OLE 800040005 "unspecified" error on the CentimetersToPoint call when executed in Delphi (XE), the similar VBS or VBA version passes
var w : OleVariant;
w := CreateOleObject('Word.Application');
w.Visible := true;
Writeln(w.CentimetersToPoints(2.0));
FWIW the type library gives
/ [id(0x00000173), helpcontext(0x09700173)]
// single CentimetersToPoints([in] single Centimeters);
By default, Delphi only passes the floating values as Double, so I tried calling IDispatch.Invoke directly and passing the argument as VT_R4, but without better results.
edit: VB version that works (save to .vbs)
set w = CreateObject("Word.Application")
w.Visible = true
msgbox w.CentimetersToPoints(2.0)
Any other suggestions of what could be going wrong?

I initially suspected that the issue is that the function expects Single and Delphi converts your float to something else. When I tracked it down in the debugger I find that the variant being passed to Invoke is has VType of varCurrency and a currency value of 2. Quite how that happens I'm not sure!
As I discovered, answering this question, it's surprisingly tricky to get a single precision float into a variant. I initially suspected that you can use the solution I presented there to solve your problem.
function VarFromSingle(const Value: Single): Variant;
begin
VarClear(Result);
TVarData(Result).VSingle := Value;
TVarData(Result).VType := varSingle;
end;
....
w := CreateOleObject('Word.Application');
w.Visible := true;
Writeln(w.CentimetersToPoints(VarFromSingle(2.0)));
But this fails also, in the same way, for reasons I don't yet understand.
Like you, I tried calling the function using IDispatch.Invoke. This is what I came up with:
program SO16279098;
{$APPTYPE CONSOLE}
uses
SysUtils, Variants, Windows, ComObj, ActiveX;
function VarFromSingle(const Value: Single): Variant;
begin
VarClear(Result);
TVarData(Result).VSingle := Value;
TVarData(Result).VType := varSingle;
end;
var
WordApp: Variant;
param: Variant;
retval: HRESULT;
disp: IDispatch;
Params: TDispParams;
result: Variant;
begin
try
CoInitialize(nil);
WordApp := CreateOleObject('Word.Application');
disp := IDispatch(WordApp);
param := VarFromSingle(2.0);
Params := Default(TDispParams);
Params.cArgs := 1;
Params.rgvarg := #param;
retval := disp.Invoke(
371,//CentimetersToPoints
GUID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_METHOD,
Params,
#Result,
nil,
nil
);
// retval = E_FAIL
Params := Default(TDispParams);
retval := disp.Invoke(
404,//ProductCode
GUID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_METHOD,
Params,
#Result,
nil,
nil
);
// retval = S_OK
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
I cannot call CentimetersToPoints this way, but can call the ProductCode function.
To add to your collection of success/failure indicators, when I call CentimetersToPoints function using PowerShell I have success. When I call using Python's win32com.client, I get E_FAIL.
There is clearly some special magic ingredient that we are missing. It seems that all the MS tools know about this magic.
I conclude that it is not possible to call CentimetersToPoints using variant dispatch as implemented in Delphi. It does not know about the magic, whatever that magic is.
It is clearly possible to call Invoke on the IDispatch and succeed. We can tell that because other environments manage to do so. So, what I do not know yet is what the missing magic is.
If you could use early bound COM, then you could sidestep this issue:
Writeln((IDispatch(w) as WordApplication).CentimetersToPoints(2.0));
OK, with the help of Hans Passant, I have some Delphi code that manages to call this function:
program SO16279098;
{$APPTYPE CONSOLE}
uses
SysUtils, Variants, Windows, ComObj, ActiveX;
function VarFromSingle(const Value: Single): Variant;
begin
VarClear(Result);
TVarData(Result).VSingle := Value;
TVarData(Result).VType := varSingle;
end;
var
WordApp: Variant;
param: Variant;
retval: HRESULT;
disp: IDispatch;
Params: TDispParams;
result: Variant;
begin
try
CoInitialize(nil);
WordApp := CreateOleObject('Word.Application');
disp := IDispatch(WordApp);
param := VarFromSingle(2.0);
Params := Default(TDispParams);
Params.cArgs := 1;
Params.rgvarg := #param;
retval := disp.Invoke(
371,//CentimetersToPoints
GUID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_METHOD or DISPATCH_PROPERTYGET,
Params,
#Result,
nil,
nil
);
Writeln(Result);
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
For reasons unknown, you need to include DISPATCH_PROPERTYGET as well as DISPATCH_METHOD.
The question that I asked probably makes this question a duplicate. So, I'm voting to close.

Related

How to invoke a VBscript function from pointer obtained by Getref in a Delphi COM server app

I have a COM server App that and need to link callbacks to specific events which are late bound.
My test VB script is as follows
Sub Main
dim Frm
Set Frm=NewForm("Form1")
Frm.OnActivate = getRef("Frm_OnActivate")
a= Frm.Showmodal
end Sub
sub Frm_OnActivate
MsgBox("Activate")
end Sub
My com server has the onActivate property which is of type OleVariant.
function TALform.Get_OnActivate: OleVariant;
begin
result:=FonActivate;
end;
procedure TALform.Set_OnActivate(Value: OleVariant);
begin
FonActivate:=Value;
Fform.OnActivate:=OnactivateEx
end;
My question is, having got that value, how do I call the VBscript function from the value stored in the Olevariant (which the debugger shows to be of type VarDispatch) ?
Try something like this:
var
Param: TDispParams;
MethodResult: OleVariant;
Result: HRESULT;
begin
Param.rgvarg := nil;
Param.rgdispidNamedArgs := nil;
Param.cArgs := 0;
Param.cNamedArgs := 0;
Result := IDispatch(FonActivate).Invoke(0, GUID_NULL, SysLocale.DefaultLCID, DISPATCH_METHOD, Param, #MethodResult, nil, nil);
end;

How to determine the size of a buffer for a DLL call when the result comes from the DLL

Using both Delphi 10.2 Tokyo and Delphi XE2.
I have a DLL that posts XML data to a site. The DLL is built with Delphi 10 in order to use TLS 1.2, which is not available with Delphi XE2.
The call to the DLL comes from a Delphi XE2 EXE, but I don't believe that is relevant, but I am noting it nonetheless.
The call to post data to a site will often return text data. Sometimes very large amounts of text data. Greater than 150K characters.
My original DLL convention was basically not correct, as I returned the contents of the returned text data as a PChar. In my readings here and elsewhere, that's a big no-no.
That "bad" methodology worked well until I started to get very large amounts of data returned. I tested it, and it failed on anything greater than 132,365 characters.
I restructured my DLL and calling code to pass in a buffer as a PChar to fill in, but I get an error trying to fill the output value!
Secondly, since I never know how big the returned data will be, how to I specify how big a buffer to fill from my calling method?
My DLL code where I get the error:
library TestDLL;
uses
SysUtils,
Classes,
Windows,
Messages,
vcl.Dialogs,
IdSSLOpenSSL, IdHTTP, IdIOHandlerStack, IdURI,
IdCompressorZLib;
{$R *.res}
function PostAdminDataViaDll(body, method, url: PChar; OutData : PChar; OutLen : integer): integer; stdcall
var HTTPReq : TIdHTTP;
var Response: TStringStream;
var SendStream : TStringStream;
var IdSSLIOHandler : TIdSSLIOHandlerSocketOpenSSL;
var Uri : TIdURI;
var s : string;
begin
Result := -1;
try
HTTPReq := TIdHTTP.Create(nil);
IdSSLIOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
IdSSLIOHandler.SSLOptions.Mode := sslmClient;
IdSSLIOHandler.SSLOptions.SSLVersions := [sslvTLSv1_2, sslvTLSv1_1];
if Assigned(HTTPReq) then begin
HTTPReq.Compressor := TIdCompressorZLib.Create(HTTPReq);
HTTPReq.IOHandler := IdSSLIOHandler;
HTTPReq.ReadTimeout := 180000;//set read timeout to 3 minutes
HTTPReq.Request.ContentType := 'text/xml;charset=UTF-8';
HTTPReq.Request.Accept := 'text/xml';
HTTPReq.Request.CustomHeaders.AddValue('SOAPAction', 'http://tempuri.org/Administration/' + method);
HTTPReq.HTTPOptions := [];
end;
SendStream := TStringStream.Create(Body);
Response := TStringStream.Create(EmptyStr);
try
HTTPReq.Request.ContentLength := Length(Body);
Uri := TiDUri.Create(url);
try
HTTPReq.Request.Host := Uri.Host;
finally
Uri.Free;
end;
HTTPReq.Post(url + 'admin.asmx', SendStream,Response);
if Response.Size > 0 then begin
if assigned(OutData) then begin
s := Response.DataString;// Redundant? Probably can just use Response.DataString?
StrPLCopy(OutData, s, OutLen);// <- ACCESS VIOLATION HERE
//StrPLCopy(OutData, s, Response.Size);// <- ACCESS VIOLATION HERE
Result := 0;
end;
end
else begin
Result := -2;
end;
finally
Response.Free;
SendStream.Free;
IdSSLIOHandler.Free;
HTTPReq.Free;
end;
except
on E:Exception do begin
ShowMessage(E.Message);
Result := 1;
end;
end;
end;
exports
PostAdminDataViaDll;
begin
end.
My Calling method code:
function PostAdminData(body, method, url : string): IXMLDOMDocument;
type
TMyPost = function (body, method, url: PChar; OutData : PChar; OutLen : integer): integer; stdcall;
var Handle : THandle;
var MyPost : TMyPost;
var dataString : string;
var returnData : string;
begin
if not (FileExists(ExtractFilePath(Application.ExeName) + 'TestDLL.DLL')) then begin
Application.MessageBox(pchar('Unable to find TestDLL.DLL.'), pchar('Error posting'),MB_ICONERROR + MB_OK);
Exit;
end;
dataString := EmptyStr;
returnData := '';
Handle := LoadLibrary(PChar(ExtractFilePath(Application.ExeName) + 'TestDLL.DLL'));
if Handle <> 0 then begin
try
try
MyPost := GetProcAddress(Handle, 'PostAdminDataViaDll');
if #MyPost <> nil then begin
// NOTE 32767 is not big enough for the returned data! Help!
if MyPost(PChar(body), PChar(method), PChar(url), PChar(returnData), 32767) = 0 then begin
dataString := returnData;
end;
end;
except
end;
finally
FreeLibrary(Handle);
end;
end
else begin
Application.MessageBox(pchar('Unable to find TestDLL.DLL.'), pchar('Error posting'),MB_ICONERROR + MB_OK);
end;
if not sametext(dataString, EmptyStr) then begin
try
Result := CreateOleObject('Microsoft.XMLDOM') as IXMLDOMDocument;
Result.async := False;
Result.loadXML(dataString);
except
end;
end;
end;
I have a DLL that posts XML data to a site. The DLL is built with Delphi 10 in order to use TLS 1.2, which is not available with Delphi XE2.
Why not simply update Indy in XE2 to a newer version that supports TLS 1.2? Then you don't need the DLL at all.
My original DLL convention was basically not correct, as I returned the contents of the returned text data as a PChar. In my readings here and elsewhere, that's a big no-no.
It is not a "big no-no", especially if the response data is dynamic in nature. Returning a pointer to dynamically allocated data is perfectly fine. You would simply have to export an extra function to free the data when the caller is done using it, that's all. The "big no-no" is that this does introduce a potential memory leak, if the caller forgets to call the 2nd function. But that is what try..finally is good for.
That "bad" methodology worked well until I started to get very large amounts of data returned. I tested it, and it failed on anything greater than 132,365 characters.
That is not a lot of memory. Any failure you were getting with it was likely due to you simply misusing the memory.
I restructured my DLL and calling code to pass in a buffer as a PChar to fill in, but I get an error trying to fill the output value!
That is because you are not filling in the memory correctly.
Secondly, since I never know how big the returned data will be, how to I specify how big a buffer to fill from my calling method?
You can't, when using POST. You would have to cache the response data somewhere off to the side, and then expose ways to let the caller query that cache for its size and data afterwards.
My DLL code where I get the error:
My Calling method code:
I see a number of logic mistakes in that code.
But, the most important reason for the Access Violation error is that your EXE is simply not allocating any memory for its returnData variable.
Casting a string to a PChar never produces a nil pointer. If the input string is not empty, a pointer to the string's first Char is returned. Otherwise, a pointer to a static #0 Char is returned instead. This ensures that a string casted to PChar always results in a non-nil, null-terminated, C style character string.
Your EXE is telling the DLL that returnData can hold up to 32767 chars, but in reality it can't hold any chars at all! In the DLL, OutData is not nil, and OutLen is wrong.
Also, StrPLCopy() always null-terminates the output, but the MaxLen parameter does not include the null-terminator, so the caller must allocate room for MaxLen+1 characters. This is stated in the StrPLCopy() documentation.
With all of this said, try something more like this:
library TestDLL;
uses
SysUtils,
Classes,
Windows,
Messages,
Vcl.Dialogs,
IdIOHandlerStack, IdSSLOpenSSL, IdHTTP, IdCompressorZLib;
{$R *.res}
function PostAdminDataViaDll(body, method, url: PChar;
var OutData : PChar): integer; stdcall;
var
HTTPReq : TIdHTTP;
SendStream : TStringStream;
IdSSLIOHandler : TIdSSLIOHandlerSocketOpenSSL;
s : string;
begin
OutData := nil;
try
HTTPReq := TIdHTTP.Create(nil);
try
IdSSLIOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(HTTPReq);
IdSSLIOHandler.SSLOptions.Mode := sslmClient;
IdSSLIOHandler.SSLOptions.SSLVersions := [sslvTLSv1, sslvTLSv1_1, sslvTLSv1_2];
HTTPReq.IOHandler := IdSSLIOHandler;
HTTPReq.Compressor := TIdCompressorZLib.Create(HTTPReq);
HTTPReq.ReadTimeout := 180000;//set read timeout to 3 minutes
HTTPReq.HTTPOptions := [];
HTTPReq.Request.ContentType := 'text/xml';
HTTPReq.Request.Charset := 'UTF-8';
HTTPReq.Request.Accept := 'text/xml';
HTTPReq.Request.CustomHeaders.AddValue('SOAPAction', 'http://tempuri.org/Administration/' + method);
SendStream := TStringStream.Create(Body, TEncoding.UTF8);
try
s := HTTPReq.Post(string(url) + 'admin.asmx', SendStream);
finally
SendStream.Free;
end;
Result := Length(s);
if Result > 0 then begin
GetMem(OutData, (Result + 1) * Sizeof(Char));
Move(PChar(s)^, OutData^, (Result + 1) * Sizeof(Char));
end;
finally
HTTPReq.Free;
end;
except
on E: Exception do begin
ShowMessage(E.Message);
Result := -1;
end;
end;
end;
function FreeDataViaDll(Data : Pointer): integer; stdcall;
begin
try
FreeMem(Data);
Result := 0;
except
on E: Exception do begin
ShowMessage(E.Message);
Result := -1;
end;
end;
end;
exports
PostAdminDataToCenPosViaDll,
FreeDataViaDll;
begin
end.
function PostAdminData(body, method, url : string): IXMLDOMDocument;
type
TMyPost = function (body, method, url: PChar; var OutData : PChar): integer; stdcall;
TMyFree = function (Data Pointer): integer; stdcall;
var
hDll : THandle;
MyPost : TMyPost;
MyFree : TMyFree;
dataString : string;
returnData : PChar;
returnLen : Integer;
begin
hDll := LoadLibrary(PChar(ExtractFilePath(Application.ExeName) + 'TestDLL.DLL'));
if hDll = 0 then begin
Application.MessageBox('Unable to load TestDLL.DLL.', 'Error posting', MB_ICONERROR or MB_OK);
Exit;
end;
try
try
MyPost := GetProcAddress(hDll, 'PostAdminDataViaDll');
MyFree := GetProcAddress(hDll, 'FreeDataViaDll');
if Assigned(MyPost) and Assigned(MyFree) then begin
returnLen := MyPost(PChar(body), PChar(method), PChar(url), returnData);
if returnLen > 0 then begin
try
SetString(dataString, returnData, returnLen);
finally
MyFree(returnData);
end;
end;
end;
finally
FreeLibrary(hDll);
end;
except
end;
if dataString <> '' then begin
try
Result := CreateOleObject('Microsoft.XMLDOM') as IXMLDOMDocument;
Result.async := False;
Result.loadXML(dataString);
except
end;
end;
end;

How to get TWebBrowser error information

I'm implementing the Exec method of TWebBrowser based on this answer. This method is triggered whenever a script error occurs. Now I need to get error information.
I first get hold of the event object of the TWebBrowser.
var
Doc: IHTMLDocument2;
HTMLWindow: IHTMLWindow2;
Event: IHTMLEventObj;
MethodName: String;
MethodResult: OleVariant;
DispatchId: Integer;
Param: array of OleVariant;
begin
//Avoid non-error calls
if nCmdID != OLECMDID_SHOWSCRIPTERROR then
Exit;
//Get hold of the event object
Doc := MapForm.WebBrowser.Document as IHTMLDocument2;
if not Assigned(Doc) then
Exit;
HTMLWindow := Doc.parentWindow;
if not Assigned(HTMLWindow) then
Exit;
//Get the current event
Event := HTMLWindow.event;
And then I'm trying to get the information I need (as demonstrated in this link) using GetIDsOfNames and Invoke functions of the interface. A working Delphi code for using these methods are in this documentation link.
Here is how I use these functions on the Event object.
MethodName := 'errorMessage';
Result := Event.GetIDsOfNames(GUID_NULL, #MethodName, 1, SysLocale.DefaultLCID, #DispatchId);
Result := Event.Invoke(DispatchId, GUID_NULL, SysLocale.DefaultLCID, DISPATCH_METHOD, Param, #MethodResult, nil, nil);
The GetIDsOfNames fuGetIDsOfNames function executes properly, outputs an acceptable integer to DispatchId and returns S_OK.
But the Invoke function just fails. It returns some negative integer as HRESULT and doesn't output anything to MethodResult.
How can I work around this?
The error values you are trying to access are not object methods, they are properties, so Invoke() is going to fail due to your use of DISPATCH_METHOD. Use DISPATCH_PROPERTYGET instead.
However, OleVariant (and Variant) has built-in support for IDispatch.Invoke(), so you don't need to mess with it manually at all. You can call object methods and read/write object properties normally, and the compiler will produce the necessary IDispatch calls for you.
Try something more like this:
function TWebBrowser.Exec(CmdGroup: PGUID; nCmdID, nCmdexecopt: DWORD;
const vaIn: OleVariant; var vaOut: OleVariant): HRESULT; stdcall;
var
Event: OleVariant;
ErrorLine: Integer;
ErrorChar: Char;
ErrorCode: Integer;
ErrorMsg: String;
ErrorUrl: String;
begin
if (CmdGroup = nil) or not IsEqualGUID(CmdGroup^, CGID_DocHostCommandHandler) then
begin
Result := OLECMDERR_E_UNKNOWNGROUP;
Exit;
end;
if nCmdID <> OLECMDID_SHOWSCRIPTERROR then
begin
Result := OLECMDERR_E_NOTSUPPORTED;
Exit;
end;
Event := (IUnknown(vaIn) as IHTMLDocument2).parentWindow.event;
ErrorLine := Event.errorLine;
ErrorChar := Event.errorCharacter;
ErrorCode := Event.errorCode;
ErrorMsg := Event.errorMessage;
ErrorUrl := Event.errorUrl;
...
if (should continue running scripts) then
begin
vaOut := True;
end else
begin
vaOut := False;
end;
Result := S_OK;
end;

Delphi's TPerlRegEx.EscapeRegExChars() always return an empty string?

With Delphi XE4, try the following code:
procedure TForm3.Button1Click(Sender: TObject);
var
myStr: string;
begin
Edit1.Text := TPerlRegEx.EscapeRegExChars('test');
end;
The result (Edit1.Text) is empty.
Is this a bug or I'm missing something? I previously had no problem with this TPerlRegEx.EscapeRegExChars function with the version from regular-expressions.info pre-DelphiXE.
Update 2: Just upgrading an app written in D2010 and encountering this bug, but just wondering how such an obvious bug can exist this long... now I'm seriously considering making my code compatible to Free Pascal, but I really like the antonymous method...
Update 1: I'm using Delphi XE4 Update 1.
It appears to be a bug. If that's the case, both the XE4 and XE5 versions contain it. I've opened a QC report to report it for XE4..XE6.
The problem appears to be with the last line of the function:
Result.Create(Tmp, 0, J);
Stepping through in the debugger shows that the Tmp (a TCharArray) correctly contains 't','e','s','t', #0, #0, #0, #0 at that point, yet Result contains '' when the function actually returns, as setting a breakpoint on the end; following that line indicates that result contains '' at that point (and when the function returns).
Providing a replacement version in a class helper with a minor change to actually store the return value from the call to Create fixes the problem:
type
TPerlRegExHelper = class helper for TPerlRegEx
public
class function EscapeRegExCharsEx(const S: string): string; static;
end;
class function TPerlRegExHelper.EscapeRegExCharsEx(const S: string): string;
var
I, J: Integer;
Tmp: TCharArray;
begin
SetLength(Tmp, S.Length * 2);
J := 0;
for I := Low(S) to High(S) do
begin
case S[I] of
'.', '[', ']', '(', ')', '?', '*', '+', '{', '}', '^', '$', '|', '\':
begin
Tmp[J] := '\';
Inc(j);
Tmp[J] := S[I];
end;
#0:
begin
Tmp[J] := '\';
Inc(j);
Tmp[J] := '0';
end;
else
Tmp[J] := S[I];
end;
Inc(J);
end;
{ Result.Create(Tmp, 0, J); } // The problem code from the original
Result := String.Create(Tmp, 0, J);
end;
The XE3 (and the open-source version you mention) implement the logic totally differently, using the more standard manipulation of Result beginning at the first line of the function with Result := S;, and then using System.Insert as needed to add room for the escape characters.
This is a bug introduced in the XE4 release that is still present in XE6. Previous versions were fine. It looks like the changes were made in readiness for some future switch to immutable strings.
Rather ironically the bug is caused by the string never being assigned a value at all. It's one thing to set out not to mutate a string, but quite another never to initialize it!
So to the analysis of the bug. The method in question in TPerlRegEx.EscapeRegExChars defined in the System.RegularExpressionsCore unit. This is a class function that returns a string. Its signature is:
class function EscapeRegExChars(const S: string): string;
The XE4 implementation makes but one reference to the result variable. As follows:
Result.Create(Tmp, 0, J);
Here, Tmp is an array of char containing the escaped text to be returned, and J is the length of that text.
So, it seems clear that the author intended for this code to assign to the function return variable Result. Sadly that does not occur. Why not? Well, the Create method being called is defined in the helper for string. This is TStringHelper defined in the System.SysUtils unit. There are three Create overloads and the one in play here is:
class function Create(const Value: array of Char; StartIndex: Integer;
Length: Integer): string; overload; static;
Note that this is a class static function. That means that it is not an instance method and has no Self pointer. So when called like this:
Result.Create(Tmp, 0, J);
It is simply a function call whose return value is ignored. It might appear that the result variable would be set but remember that this Create is a class static method. It therefore has no instance. The compiler simply uses the type of Result to resolve the method. The code is equivalent to:
string.Create(Tmp, 0, J);
Nothing more exciting than a call to a function whose return value is simply ignored. Defeated by the extended syntax that allows us to ignore function return values.
The fix to the code is simple enough. Replace that final line with
Result := string.Create(Tmp, 0, J);
You could apply the fix in a copy of the unit, and include that unit in your code. An alternative to that, my preferred option, is to use a code hook. Like this:
unit FixTPerlRegExEscapeRegExChars;
interface
implementation
uses
System.SysUtils, Winapi.Windows, System.RegularExpressionsCore;
procedure PatchCode(Address: Pointer; const NewCode; Size: Integer);
var
OldProtect: DWORD;
begin
if VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, OldProtect) then
begin
Move(NewCode, Address^, Size);
FlushInstructionCache(GetCurrentProcess, Address, Size);
VirtualProtect(Address, Size, OldProtect, #OldProtect);
end;
end;
type
PInstruction = ^TInstruction;
TInstruction = packed record
Opcode: Byte;
Offset: Integer;
end;
procedure RedirectProcedure(OldAddress, NewAddress: Pointer);
var
NewCode: TInstruction;
begin
NewCode.Opcode := $E9;//jump relative
NewCode.Offset := NativeInt(NewAddress)-NativeInt(OldAddress)-SizeOf(NewCode);
PatchCode(OldAddress, NewCode, SizeOf(NewCode));
end;
function EscapeRegExChars(Self: TPerlRegEx; const S: string): string;
var
I, J: Integer;
Tmp: TCharArray;
begin
SetLength(Tmp, S.Length * 2);
J := 0;
for I := Low(S) to High(S) do
begin
case S[I] of
'.', '[', ']', '(', ')', '?', '*', '+', '{', '}', '^', '$', '|', '\':
begin
Tmp[J] := '\';
Inc(j);
Tmp[J] := S[I];
end;
#0:
begin
Tmp[J] := '\';
Inc(j);
Tmp[J] := '0';
end;
else
Tmp[J] := S[I];
end;
Inc(J);
end;
Result := string.Create(Tmp, 0, J);
end;
initialization
RedirectProcedure(#TPerlRegEx.EscapeRegExChars, #EscapeRegExChars);
end.
Add this unit to your project and the calls to TPerlRegEx.EscapeRegExChars will start working again.
{$APPTYPE CONSOLE}
uses
System.RegularExpressionsCore,
FixTPerlRegExEscapeRegExChars in 'FixTPerlRegExEscapeRegExChars.pas';
begin
Writeln(TPerlRegEx.EscapeRegExChars('test'));
Readln;
end.
Output
test
QC#124091

Serial port enumeration in Delphi using SetupDiGetClassDevs

I'm trying to enumerate "friendly names" for COM ports. The ports may dynamically change as USB-serial devices are connected and disconnected at runtime.
Based on the possible methods described in this question, I am attempting to use the SetupDiGetClassDevs method.
I found this example code, but it is written for an older version of the setupapi unit (the original link to homepages.borland.com doesn't work of course).
I tried using the setupapi unit from the current JVCL(JVCL340CompleteJCL221-Build3845), but it doesn't seem to be compatible with Delphi 7. I get compiler errors:
if SetupDiGetDeviceRegistryProperty(DevInfoHandle,DeviceInfoData,
RegProperty,
#PropertyRegDataType,
#S1[1],RequiredSize,#RequiredSize) then begin
In the call to function SetupDiGetDeviceRegistryProperty,
I get the error "Types of actual and formal parameters must be identical" on the parameters #PropertyRegDataType, and #RequiredSize.
The Delphi3000 site says the code was written in 2004 and is intended for Delphi 7, so I'm not sure why it doesn't work with Delphi 7 now, unless setupapi has changed. Is anyone familiar with the changes to setupapi that could cause these problems?
I'm testing with a simple console program. The uses statement is " windows,
sysutils,
classes,
setupAPI,
Registry;"
The main program is:
begin
ComPortStringList := SetupEnumAvailableComPorts;
for Index := 0 to ComPortStringList.Count - 1 do
writeln(ComPortStringList[Index]);
end;
end.
The following procedure is working correctly for me (in Windows 8.1). It is important to use the parameter KEY_READ in the TRegistry.Constructor.
procedure EnumComPorts(const Ports: TStringList);
var
nInd: Integer;
begin { EnumComPorts }
with TRegistry.Create(KEY_READ) do
try
RootKey := HKEY_LOCAL_MACHINE;
if OpenKey('hardware\devicemap\serialcomm', False) then
try
Ports.BeginUpdate();
try
GetValueNames(Ports);
for nInd := Ports.Count - 1 downto 0 do
Ports.Strings[nInd] := ReadString(Ports.Strings[nInd]);
Ports.Sort()
finally
Ports.EndUpdate()
end { try-finally }
finally
CloseKey()
end { try-finally }
else
Ports.Clear()
finally
Free()
end { try-finally }
end { EnumComPorts };
I was able to get some more specific suggestions by asking the question a different way with different tags.
It turns out there were errors in the delphi3000.com example code, and possibly errors in the JVCL code. After fixing the example code errors, I got it to work. I have not addressed the potential JVCL errors.
Here is the working code (as a simple console app) for enumerating the names of com ports:
{$APPTYPE CONSOLE}
program EnumComPortsTest;
uses
windows,
sysutils,
classes,
setupAPI,
Registry;
{$R *.RES}
var
ComPortStringList : TStringList;
(*
The function below returns a list of available COM-ports
(not open by this or an other process), with friendly names. The list is formatted as follows:
COM1: = Communications Port (COM1)
COM5: = NI Serial Port (Com5)
COM6: = NI Serial Port (Com6)
COM7: = USB Serial Port (COM7)
COM8: = Bluetooth Communications Port (COM8)
COM9: = Bluetooth Communications Port (COM9)
This code originally posted at http://www.delphi3000.com/articles/article_4001.asp?SK=
errors have been fixed so it will work with Delphi 7 and SetupAPI from JVCL
*)
function SetupEnumAvailableComPorts:TstringList;
// Enumerates all serial communications ports that are available and ready to
// be used.
// For the setupapi unit see
// http://homepages.borland.com/jedi/cms/modules/apilib/visit.php?cid=4&lid=3
var
RequiredSize: Cardinal;
GUIDSize: DWORD;
Guid: TGUID;
DevInfoHandle: HDEVINFO;
DeviceInfoData: TSPDevInfoData;
MemberIndex: Cardinal;
PropertyRegDataType: DWord;
RegProperty: Cardinal;
RegTyp: Cardinal;
Key: Hkey;
Info: TRegKeyInfo;
S1,S2: string;
hc: THandle;
begin
Result:=Nil;
//If we cannot access the setupapi.dll then we return a nil pointer.
if not LoadsetupAPI then exit;
try
// get 'Ports' class guid from name
GUIDSize := 1; // missing from original code - need to tell function that the Guid structure contains a single GUID
if SetupDiClassGuidsFromName('Ports',#Guid,GUIDSize,RequiredSize) then begin
//get object handle of 'Ports' class to interate all devices
DevInfoHandle:=SetupDiGetClassDevs(#Guid,Nil,0,DIGCF_PRESENT);
if Cardinal(DevInfoHandle)<>Invalid_Handle_Value then begin
try
MemberIndex:=0;
result:=TStringList.Create;
//iterate device list
repeat
FillChar(DeviceInfoData,SizeOf(DeviceInfoData),0);
DeviceInfoData.cbSize:=SizeOf(DeviceInfoData);
//get device info that corresponds to the next memberindex
if Not SetupDiEnumDeviceInfo(DevInfoHandle,MemberIndex,DeviceInfoData) then
break;
//query friendly device name LIKE 'BlueTooth Communication Port (COM8)' etc
RegProperty:=SPDRP_FriendlyName;{SPDRP_Driver, SPDRP_SERVICE, SPDRP_ENUMERATOR_NAME,SPDRP_PHYSICAL_DEVICE_OBJECT_NAME,SPDRP_FRIENDLYNAME,}
SetupDiGetDeviceRegistryProperty(DevInfoHandle,
DeviceInfoData,
RegProperty,
PropertyRegDataType,
NIL,0,RequiredSize);
SetLength(S1,RequiredSize);
if SetupDiGetDeviceRegistryProperty(DevInfoHandle,DeviceInfoData,
RegProperty,
PropertyRegDataType,
#S1[1],RequiredSize,RequiredSize) then begin
KEY:=SetupDiOpenDevRegKey(DevInfoHandle,DeviceInfoData,DICS_FLAG_GLOBAL,0,DIREG_DEV,KEY_READ);
if key<>INValid_Handle_Value then begin
FillChar(Info, SizeOf(Info), 0);
//query the real port name from the registry value 'PortName'
if RegQueryInfoKey(Key, nil, nil, nil, #Info.NumSubKeys,#Info.MaxSubKeyLen, nil, #Info.NumValues, #Info.MaxValueLen,
#Info.MaxDataLen, nil, #Info.FileTime) = ERROR_SUCCESS then begin
RequiredSize:= Info.MaxValueLen + 1;
SetLength(S2,RequiredSize);
if RegQueryValueEx(KEY,'PortName',Nil,#Regtyp,#s2[1],#RequiredSize)=Error_Success then begin
If (Pos('COM',S2)=1) then begin
//Test if the device can be used
hc:=CreateFile(pchar('\\.\'+S2+#0),
GENERIC_READ or GENERIC_WRITE,
0,
nil,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
0);
if hc<> INVALID_HANDLE_VALUE then begin
Result.Add(Strpas(PChar(S2))+': = '+StrPas(PChar(S1)));
CloseHandle(hc);
end;
end;
end;
end;
RegCloseKey(key);
end;
end;
Inc(MemberIndex);
until False;
//If we did not found any free com. port we return a NIL pointer.
if Result.Count=0 then begin
Result.Free;
Result:=NIL;
end
finally
SetupDiDestroyDeviceInfoList(DevInfoHandle);
end;
end;
end;
finally
UnloadSetupApi;
end;
end;
var
index : integer;
begin
ComPortStringList := SetupEnumAvailableComPorts;
if (ComPortStringList <> nil) and (ComPortStringList.Count > 0) then
for Index := 0 to ComPortStringList.Count - 1 do
writeln(ComPortStringList[Index]);
end.
Looks like some arguments of type PDWord were replaced by var DWord in SetupApi.pas. All you need is to remove '#' from these arguments in your code like that:
if SetupDiGetDeviceRegistryProperty(DevInfoHandle,DeviceInfoData,
RegProperty,
PropertyRegDataType,
#S1[1],RequiredSize,RequiredSize) then begin
Do you have "typed # operator" turned on? Project options, Compiler tab under "Syntax options". A lot of third party code breaks if that option is enabled.
For easier operation you might consider simply using the registry where those names are listed eg:
ErrCode := RegOpenKeyEx(
HKEY_LOCAL_MACHINE,
'HARDWARE\DEVICEMAP\SERIALCOMM',
0,
KEY_READ,
KeyHandle);
(I've ommited the hand-waving stuff).
You might also consider using WMI - see this example from Magenta Systems - you can get a pretty much everything hardware-related now.
I adapted below code from RRUZ answer for Serial Port class. Works fine under Win10 20H2.
{$APPTYPE CONSOLE}
uses
SysUtils,
ActiveX,
ComObj,
Variants;
procedure GetWin32_SerialPortInfo;
const
WbemUser ='';
WbemPassword ='';
WbemComputer ='localhost';
wbemFlagForwardOnly = $00000020;
var
FSWbemLocator : OLEVariant;
FWMIService : OLEVariant;
FWbemObjectSet: OLEVariant;
FWbemObject : OLEVariant;
oEnum : IEnumvariant;
iValue : LongWord;
begin;
FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
FWMIService := FSWbemLocator.ConnectServer(WbemComputer, 'root\CIMV2', WbemUser, WbemPassword);
FWbemObjectSet:= FWMIService.ExecQuery('SELECT * FROM Win32_SerialPort','WQL',wbemFlagForwardOnly);
oEnum := IUnknown(FWbemObjectSet._NewEnum) as IEnumVariant;
while oEnum.Next(1, FWbemObject, iValue) = 0 do
begin
// for other fields: https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-serialport
Writeln(Format('DeviceID %s',[String(FWbemObject.DeviceID)]));// String
Writeln(Format('Name %s',[String(FWbemObject.Name)]));// String
Writeln(Format('Description %s',[String(FWbemObject.Description)]));// String
FWbemObject:=Unassigned;
end;
end;
begin
try
CoInitialize(nil);
try
GetWin32_SerialPortInfo;
finally
CoUninitialize;
end;
except
on E:EOleException do
Writeln(Format('EOleException %s %x', [E.Message,E.ErrorCode]));
on E:Exception do
Writeln(E.Classname, ':', E.Message);
end;
Writeln('Press Enter to exit');
Readln;
end.
Output:
DeviceID COM7
Name Silicon Labs CP210x USB to UART Bridge (COM7)
Description Silicon Labs CP210x USB to UART Bridge
Press Enter to exit

Resources