How do I pass nil to a var parameter?
Asked Answered
G

2

7

There are quite a few API routines that take a pointer to some variable as a parameter that were translated to var parameters, yet can be specified as nil pointers according to the Windows help files.

As an example, the ChangeDisplaySettings function is declared as:

function ChangeDisplaySettings(var lpDevMode: TDeviceMode; dwFlags: DWORD): Longint; stdcall;

But the Windows help file clearly states that "Passing NULL for the lpDevMode parameter is the easiest way to return to the default mode after a dynamic mode change." The correct translation should have been:

function ChangeDisplaySettings(lpDevMode: PDeviceMode; dwFlags: DWORD): Longint; stdcall;

I'm posting this question and answer to help newbies get around these issues without having to re-declare the functions. I still remember that it was an issue for me in the beginning.

Graeco answered 22/11, 2015 at 21:17 Comment(3)
Re-declaring the function is the correct solutionWorry
@David, Thanks; I respect your opinion as a clearly seasoned veteran in Delphi. Personally, though, I don't like having re-declarations of standard routines all over the place and prefer to use the standard ones wherever I can. I will however update my answer to indicate that standard practice seems to prefer re-declaration.Graeco
There should be two overloads. One with a var, and one with a pointer to struct. Then the caller can choose.Worry
G
13

One solution is to re-declare any such functions using pointers in place of the var parameters, but there is an easier solution. Simply cast a dereferenced nil-pointer to the correct type, e.g., for the ChangeDisplaySettings example, use the following to reset the display mode to the default registry settings:

ChangeDisplaySettings(TDeviceMode(nil^), 0);

or

ChangeDisplaySettings(PDeviceMode(nil)^, 0);

This way you are passing a var parameter that just happens to be located at memory address zero - the compiler is happy and you get to pass a nil pointer to the API routine!

Edit: From David Hefferman's comment, it seems that the standard practice is to rather re-declare such routines. Personally, I prefer to use the standard declarations if I can in my own personal units, but for professional work purposes I suggest that developers follow the standard practices.

Graeco answered 22/11, 2015 at 21:17 Comment(8)
Personally I find the second of your options much more natural.Pyuria
@Andreas, I also prefer using the second, but listed the first for the cases where a pointer type wasn't declared. David Hefferman's preference for re-declaration, on the other hand, is the only solution that won't cause errors when a new version of Delphi fixes the standard declaration!Graeco
@Jannie - another way that this solution would break would be if the compiler were ever to be changed to correctly identify this sort of typecast trickery as breaking the parameter contract and to reject the code as invalid. The declaration provided in this case is correct only as a side effect. Never mind "standard practice", the correct solution is to use a correct declaration, not contrive to add a side effect to counteract another side effect.Heliograph
@Deltics, I agree that the correct solution is a correct declaration, but I seriously doubt that the compiler would be changed as you described. Typecasting a memory address as a pointer to some type and then dereferencing it, is quite valid to use for a var parameter. In this case we're just using nil (address 0). Of course nil does have a special meaning in Delphi, but I still doubt that they would start disallowing certain values just to prevent programmers from using the language in a non-standard way.Graeco
I would like to add that this approach doesn't work with "out" parameters, they are probably implicitly initialized when the function starts execution.Eweneck
@Molochnik, in my Delphi 7 it accepts it for out parameters. Which Delphi version did you test?Graeco
@Jannie I have 10.2 TokyoEweneck
Note this approach is documented on EDN: Passing nil to a variable parameterMarshamarshal
P
6

Aside from the other answers and comments, which are helpful, I still have another spin on this. In this instance, whomever translated this API from the header, didn't really look closely at the API documentation. Had they done that, it would have been clear that passing "nil" is a valid thing to do.

In that case, the proper course of action would be to declare a set of overloads which both reference the same import. One would be the nice "var" parameter version and the other would be the "pointer-to-structure" version. This would make it so that you could pass in a TDeviceMode variable directly (no need to take the address of the var) and still pass in nil when necessary. The compiler would "match" the "nil" to the pointer-to-structure which would then be referenced. Since both APIs resolve to the same API and the actual manner in which the parameters are being passed doesn't differ, everything still works as expected.

Since there isn't an overloaded version of that API when you should be able to pass a "nil" that is an API translation bug. Feel free to indicate this in a report at http://quality.embarcadero.com.

For the record, I've done a lot of API translations in the product over the years... it is certainly conceivable that I was the silly developer who didn't do the proper research on this one :).

Peristome answered 22/11, 2015 at 22:36 Comment(7)
Personally, I think the "nice var parameter version" is outdated. OK, it has been practice since the days of Borland Pascal, but actually, I'd prefer a translation that is closer to the original, i.e. always using pointers. This has the advantage that the docs for the API (which are almost always C-based) don't cause any confusion when, in Delphi, a "var" is used (principle of least astonishment). The docs show a pointer, the translation has a pointer. No confusion, no need to read the docs so closely. Overloads would only be required for already existing translations.Hengist
Also, it is not possible to declare an overload for a function that is not marked as overload yet, so you either have to modify the original import unit, which is not always possible, or you hide the original and, in your own unit, you declare both overloaded functions, of which one is an exact re-declaration.Hengist
That's why I suggested it be reported in Quality Portal. I never suggested that it could be done without the original having been marked as overload.Peristome
OK. I was actually trying to convince you and Embarcadero to give up the "use var instead of a pointer type where possible" principle. <g>Hengist
@Allen, Unfortunately I only have Delphi 7 at the moment; so I can't check whether it is still incorrect. Another one is IShellFolder.ParseDisplayName where dwAttributes can be nil if not needed. This one is not serious though; just supply a dummy variable. The translation error doesn't prevent any functionality.Graeco
Well, the best option is that Emba have a look at the whole Winapi.Windows. There are a lot of wrong translated api calls containing one ore more mistakes (f.i. function GetDiskFreeSpaceEx(lpDirectoryName: LPCWSTR; var lpFreeBytesAvailableToCaller, lpTotalNumberOfBytes; lpTotalNumberOfFreeBytes: PLargeInteger): BOOL; stdcall; - 2 mistakes). Well I know this will take a lot of time ... but if you know too, are you really asking us to check each api call against the api doc and file a bug report for each api call?Blackball
FYI: It should have been function GetDiskFreeSpaceEx(lpDirectoryName: LPCWSTR; lpFreeBytesAvailableToCaller, lpTotalNumberOfBytes; lpTotalNumberOfFreeBytes: PULargeInteger): BOOL; stdcall; msdn.microsoft.com/de-de/library/windows/desktop/…Blackball

© 2022 - 2024 — McMap. All rights reserved.