Dynamic Keyword Validation using ROUTINE_INFO
Anonym
In IDL, there are three variants of the _EXTRA keyword: _EXTRA, _REF_EXTRA, and _STRICT_EXTRA. The _EXTRA and _REF_EXTRA keywords are used to define the signature of the routine, while you can use either _EXTRA or _STRICT_EXTRA when you invoke any routine. As my colleague Jim Pendleton wrote about a few years ago, the _REF_EXTRA keyword is a potential improvement over plain old _EXTRA. This is because it uses pass by reference semantics instead of the pass by value semantics that _EXTRA does, so you don’t make copies of the variables at each function call. There is the downside that the values passed by _REF_EXTRA are mutable, so some care needs to be taken there, but it can save you a lot of memory and time allocating those copies.
The downside of both _EXTRA and _REF_EXTRA is that they are generic grab bags that allow the caller to call your function with any superfluous keywords they want, with no way of knowing which keywords are actually used and which are extraneous. Enter the _STRICT_EXTRA keyword, which is discussed in the help documentation of “Keyword Inheritance”. Normally when you call a routine, you use _EXTRA keyword as the way to pass in the collection of keywords to the routine. Any keywords in the _EXTRA bag that are part of the routine signature will be properly mapped, and whatever is left will be captured by _EXTRA/_REF_EXTRA if it’s present, or dropped on the floor otherwise. If you were to call the same routine using _STRICT_EXTRA instead of _EXTRA, then it will throw an error if there are any keywords that do not map to the routine signature. We can see this in action in this contrived example, which uses minimal procedures to illustrate the validation logic:
pro callByValue, _EXTRA=extra
help, extra
end
pro callByReference, _REF_EXTRA=refExtra
help, refExtra
end
pro callWithNoExtra, FOO=foo
help, foo
end
pro callWrapper, _REF_EXTRA=extra
callByValue, _EXTRA=extra
callByReference, _EXTRA=extra
callWithNoExtra, _EXTRA=extra
end
pro callStrictWrapper, _REF_EXTRA=extra
callByValue, _STRICT_EXTRA=extra
callByReference, _STRICT_EXTRA=extra
callWithNoExtra, _STRICT_EXTRA=extra
end
pro extra_tests, _REF_EXTRA=extra
callWrapper, PI=!pi
callStrictWrapper, PI=!pi
end
The output from running extra_tests is:
** Structure <1307e5a0>, 1 tags, length=4, data length=4, refs=1:
PI FLOAT 3.14159
REFEXTRA STRING = Array[1]
FOO UNDEFINED = <Undefined>
** Structure <1307e650>, 1 tags, length=4, data length=4, refs=1:
PI FLOAT 3.14159
REFEXTRA STRING = Array[1]
% Keyword PI not allowed in call to: CALLWITHNOEXTRA
% Execution halted at: CALLSTRICTWRAPPER 32 C:\Users\brian\IDLWorkspace83\Default\extra_tests.pro
% EXTRA_TESTS 43 C:\Users\brian\IDLWorkspace83\Default\extra_tests.pro
% $MAIN$
The _EXTRA and _REF_EXTRA keywords are able to accept the PI keyword without incident. When callWrapper invokes callWithNoExtra, the PI keyword is dropped on the floor, and FOO is left undefined. But when callStrictWrapper invokes callWithNoExtra, the PI keyword does not line up with the routine signature and an error is thrown. It is important to note that callByValue and callByReference work perfectly fine with _STRICT_EXTRA, as their _EXTRA and _REF_EXTRA keywords respectively will swallow up the PI keyword no problem.
So _STRICT_EXTRA has its limitations, particularly in the case where you want to call more than one routine that doesn’t include a form of _EXTRA. To accomplish this we need to query the IDL runtime to get information about the routine signatures using the powerful ROUTINE_INFO function. When you use this function with its /PARAMETERS keyword, it will return a structure that lets you get the list of all the keywords in a given routine’s signature. Let’s look at a simple example to see what ROUTINE_INFO returns:
pro myPro, PARAM1=p1, PARAM2=p2, PARAM3=p3
print, 'in myPro'
help, p1, p2, p3
end
function myFunc, PARAM2=p2, PARAM4=p4
print, 'in myFunc'
help, p2, p4
return, 0
end
IDL> info1 = Routine_Info('myPro', /PARAMETERS)
IDL> info2 = Routine_Info('myFunc', /PARAMETERS, /FUNCTIONS)
IDL> info1
{
NUM_ARGS: 0,
NUM_KW_ARGS: 3,
KW_ARGS: ["PARAM1" "PARAM2" "PARAM3"]
}
IDL> info2
{
NUM_ARGS: 0,
NUM_KW_ARGS: 2,
KW_ARGS: ["PARAM2" "PARAM4"]
}
The implied print output for a struct is convenient as it gives you the tags, but also expands arrays, which neither help nor print will do for a struct. You’ll notice that I had to add the /FUNCTIONS keyword when I was requesting info about a function instead of a procedure. So I can use the KW_ARGS member of the info struct to identify which members of the _REF_EXTRA bag to use for each routine invocation. I can also use it to verify that there aren’t any superfluous keywords passed into the wrapper.
Once you’ve verified that the keywords passed into the wrapper are valid, you then need to construct the subset of values that are to be passed into each wrapped routine. The way to do this dynamically is to manually construct a struct to pass into the _EXTRA keyword. We do this incrementally by adding only those keywords and values to a struct using CreateStruct to append new key/value pairs. When using _REF_EXTRA, you get a string array that includes the keywords used to invoke your method. To get the values for each keyword you have to use Scope_VarFetch with its /REF_EXTRA keyword to get a reference to that value in the local scope. Here is an example wrapper for the myPro and myFunc routines defined above:
function myDynamicReferenceWrapper, _REF_EXTRA=refExtra
compile_opt idl2
; first make sure _REF_EXTRA is defined, bail if not
if (~ISA(refExtra)) then return, -1
info1 = Routine_Info('myPro', /PARAMETERS)
info2 = Routine_Info('myFunc', /PARAMETERS, /FUNCTION)
; check for invalid keywords in _REF_EXTRA
foreach keyword, refExtra do begin
if ((Total(keyword eq info1.KW_ARGS) eq 0) && $
(Total(keyword eq info2.KW_ARGS) eq 0)) then begin
Message, 'Invalid keyword ' + keyword
endif
endforeach
; call myPro with the appropriate keywords from _REF_EXTRA
extra1 = {}
foreach keyword, info1.KW_ARGS do begin
w = where(keyword eq refExtra, found)
if (found gt 0) then begin
value = Scope_VarFetch(keyword, /REF_EXTRA)
extra1 = Create_Struct(extra1, keyword, value)
endif
endforeach
myPro, _EXTRA=extra1
; call myFunc with the appropriate keywords from _REF_EXTRA
extra2 = {}
foreach keyword, info2.KW_ARGS do begin
w = where(keyword eq refExtra, found)
if (found gt 0) then begin
value = Scope_VarFetch(keyword, /REF_EXTRA)
extra2 = Create_Struct(extra2, keyword, value)
endif
endforeach
return, myFunc(_EXTRA=extra2)
end
If you had concerns about the wrapped methods modifying your variables and want to use _EXTRA instead of _REF_EXTRA, then there are a couple tweaks to this wrapper:
function myDynamicValueWrapper, _EXTRA=extra
compile_opt idl2
; first make sure _EXTRA is defined, bail if not
if (~ISA(extra)) then return, -1
info1 = Routine_Info('myPro', /PARAMETERS)
info2 = Routine_Info('myFunc', /PARAMETERS, /FUNCTIONS)
; check for invalid keywords in _EXTRA
myExtraKeywords = Tag_Names(extra)
foreach keyword, myExtraKeywords do begin
if ((Total(keyword eq info1.KW_ARGS) eq 0) && $
(Total(keyword eq info2.KW_ARGS) eq 0)) then begin
Message, 'Invalid keyword ' + keyword
endif
endforeach
; call myPro with the appropriate keywords from _EXTRA
extra1 = {}
foreach keyword, info1.KW_ARGS do begin
w = where(keyword eq myExtraKeywords, found)
if (found gt 0) then begin
extra1 = Create_Struct(extra1, keyword, extra.(w[0]))
endif
endforeach
myPro, _EXTRA=extra1
; call myFunc with the appropriate keywords from _EXTRA
extra2 = {}
foreach keyword, info2.KW_ARGS do begin
w = where(keyword eq myExtraKeywords, found)
if (found gt 0) then begin
extra2 = Create_Struct(extra2, keyword, extra.(w[0]))
endif
endforeach
return, myFunc(_EXTRA=extra2)
end
Either of these wrappers could be made truly generic by adding string array parameter that was the set of routine names you want wrapped, instead of having myPro and myFunc hardcoded.