|
Introduction
While writing the second SNMP article, Creating an SNMP Component - Part 2, an issue had been encountered with the SnmpCreateSession API function. It forced the developer to pass a function pointer so that all responses could be routed to whatever function was passed. It is important to note that the use of function pointers in Visual Basic should be limited to instances when it is absolutely necessary. The method chosen to solve a problem should be well researched before deploying it into a production environment.
The solution and methodology presented in this article is only meant to be a guide and offer alternate solutions to the aforementioned topic. This is by no means a "be all, end all" solution and should be used with proper caution. Again, any developer that utilizes these techniques should understand the impact and possible ramifications of implementation.
AddressOf Operator
Syntax: [lngVal] = AddressOf procName
Example: lSession = SnmpCreateSession(0, 0, AddressOf SnmpAgentResponse, 0)
The AddressOf operator in Visual Basic returns the reference of a procedure as a long data type. In the example above, SnmpAgentResponse is a procedure defined within a module in a Visual Basic project. It is passed as a pointer into SnmpCreateSession to allow the API to make a callback so that when a response is generated from the agent it can dynamically call SnmpAgentResponse, the function pointer.
When callbacks are used, the API function being executed will expect a certain argument list. For example, in the case of SnmpCreateSession, the argument list that SnmpAgentResponse must adhere to is the parameter list of SNMPAPI_CALLBACK. This isn't a true function; it's more of a placeholder for the function pointer. Listed below is both the definition for SNMPAPI_CALLBACK and SnmpAgentResponse:
// As outlined in the MSDN
SNMPAPI_STATUS CALLBACK SNMPAPI_CALLBACK(
HSNMP_SESSION hSession, // handle to the WinSNMP session
HWND hWnd, // handle to the notification window
UINT wMsg, // window notification message number
WPARAM wParam, // type of notification
LPARAM lParam, // request identifier of PDU
LPVOID lpClientData // optional application-defined data
);
' As defined in the NetMgrVB component
Private Function SnmpAgentResponse(
ByVal lSession As Long,
ByVal hWnd As Long,
ByVal wMsg As Long,
ByVal wParam As Long,
ByVal lParam As Long,
ByVal lpClientData As Long
) As Long
As outlined, both functions are declared with the same argument list and data type definitions. The Visual Basic implementation has different variable type definitions, but the long data type will properly translate into the API function.
It is possible to pass a function pointer between procedures within a Visual Basic project; however, there is no support for calling a function by this convention. Error handling within the callback function cannot be instituted because the caller is not in the application where the callback function resides. If an error is passed to the caller, more than likely a fatal error will occur causing your application to crash. In article Q198607 of the Microsoft Knowledge Base, they attribute this kind of behavior to the fact that "the callback function is called by a thread that was not created by Visual Basic."
The same error can occur if other API calls are made from within the callback function. This issue, coupled with the necessity to use callbacks in a Visual Basic COM environment, gave way for an alternative method for implementing this functionality.
Alternative Method
Using a callback function within a COM object can be very dangerous because waiting for a response from the callback, as in the SnmpAgentResponse example, causes the necessity for a do...loop statement that checks a global variable for changes. So what happens when the API doesn't return anything for 60 seconds or anything at all? A virtual timeout value can be set where if a certain amount of iterations have occurred or the total seconds elapsed breach a specified timeframe, the do...loop statement can be exited and an error code can be returned.
Below is a more detailed discussion on how to mesh callback functions and COM methods together without being volatile to the project. For the remainder of the article, source code from the Creating a SNMP Component -- Part 2 will be used, specifically the NetMgrVB component.
The flow for a typical Get/Set action in the NetMgrVB component is as follows:
| Initiate Object | Set snmpObj = CreateObject("NetMgrVB.SNMPv1") |
| Assign Values to Properties | snmpObj.Agent = "[agent address]"
snmpObj.CommStr = "[community string]
snmpObj.Timeout = [timeout in ms]
snmpObj.Retry = [total number of retries] |
| Open Session | strRetVal = snmpObj.SnmpMgrOpen() |
| Make a Get Request | strRetVal = snmpObj.SnmpGet("[OID]") |
| Close Connection | strRetVal = snmpObj.SnmpMgrClose() |
| Destroy Object | Set snmpObj = nothing |
The intention isn't to give a tutorial over the NetMgrVB component, this is simply provided to help correlate internal functions and processes to the flow of the component. For more information on the NetMgrVB component, please read Creating a SNMP Component - Part 2.
Listed below are two functions and a snippet of code to begin with:
' Called in NetMgrVB.modSNMPAPI.SnmpOpenSession procedure
' SnmpOpenSession is called by NetMgrVB.SnmpV1.SnmpMgrOpen
lSession = SnmpCreateSession(0, 0, AddressOf SnmpAgentResponse, 0)
This is where the function pointer is first introduced to the API function. As pointed out in the AddressOf section, the procedure that follows the AddressOf operator must be defined in a module. If the procedure is located in another module, you can reference it by [moduleName].[procedureName]. Procedures defined in a form or class is not permitted.
Next is the modSNMPAPI.InitAgentProc function which handles the actual coordination of callback function:
Public Function initAgentProc(
ByVal lSession As Long,
ByVal lngPDUType As Long,
ByVal strOID As String,
ByVal varOIDValue As Variant
) As String
' Local variable declarations
' --------------------------------
Dim strRetVal As String
Dim lngRetVal As String
' Make request to agent
' This passes the requests made from SnmpV1.Get/GetNext/Set to the agent for processing
strRetVal = SnmpAgentRequest(lSession, lngPDUType, strOID, varOIDValue)
' Check to see if an error was returned
If InStr(1, strRetVal, "[NetMgrVB.Mod.", 1) > 0 Then
' Return error message
initAgentProc = strRetVal
Else
' Wait for SnmpAgentResponse to be executed
' At this point the agent has been queried a Do...Loop
' statement so that we can wait for a response from the
' callback function
Do
DoEvents ' Allows other events to continue in the
' background
Sleep APP_RESPONSE_TIME ' Sleep was put in so that in 100% of the
' CPU wasn't continuously being utilized
Loop Until (boolResponse) ' boolResponse is the global variable that
' is set to true in SnmpAgentResponse
' when it has completed it's execution
' Return response for request value
' The function that handles all of the actual processing
initAgentProc = SnmpResponseProcess(lSession)
End If
End Function
This function is responsible for handling all high-level communication with the agent. High-level means that the actual parsing isn't handled in this function, so instead it makes the calls to appropriate functions that handle parsing and waits for a response.
Once the Get/GetNext/Set methods are called from NetMgrVB.SnmpV1, InitAgentProc is called and a request has been made. The next function is called as callback from the API function.
Private Function SnmpAgentResponse(
ByVal lSession As Long,
ByVal hWnd As Long,
ByVal wMsg As Long,
ByVal wParam As Long,
ByVal lParam As Long,
ByVal lpClientData As Long
) As Long
' Assign values to global variables
lngWParam = wParam ' If wParam = 0 then a SNMP message is available
lngLParam = lParam ' Specifies the request id for the pdu being processed
boolResponse = True ' Set global to recognize response has been received
' Return Success -- must be returned
SnmpAgentResponse = SNMPAPI_SUCCESS
End Function
The only actions that occur are global variable assignment since no other processing/API calls can occur (as outlined in article Q198607 in the Microsoft Knowledge Base). However, once this has been executed, global variable boolResponse is set true and the processing can continue in procedure InitAgentProc.
So what happens when the agent doesn't return a response, o itr takes a very long time to execute? This is where adding a timeout value will help out greatly. Take a look at the initAgentProc again:
Public Function initAgentProc(...) As String
Dim lngBeginTimer as long ' New local variable: Start Timer Value
...
lngBeginTimer = Timer
...
' Wait for SnmpAgentResponse to be executed
Do
DoEvents
Sleep APP_RESPONSE_TIME
If Round(Timer - lngBeginTimer,0) >= APP_AGENT_TIMEOUT then
InitAgentProc = "[NetMgrVB.Mod.InitAgentProc][TO1]=Response Timeout has occurred"
Exit function
End if
Loop Until (boolResponse)
...
End Function
For the purpose of this article, APP_AGENT_TIMEOUT is a Constant that houses the internal time out value in seconds. If the threshold is breached, the procedure returns an error back to the calling function and request is stopped.
Recap
Here is a brief summary of the problem and solution presented:
- AddressOf Operator
- Syntax: AddressOf [procedurename]
- Returns the reference or address of a procedure as a long data type
- Procedures passed to AddressOf must be defined in a module
- Cannot actually call a function by it's function pointer
- Alternative Method
- Create four functions (or a similar implementation) that will handle:
- Passing the function pointer to an API call (SnmpOpenSession)
- The actual callback function, which will act as a placeholder (SnmpAgentResponse)
- Coordination of high-level procedure calls (initAgentProc)
- The actual processing of the response (SnmpResponseProcess)
- Additional processing might be required to trigger the mechanism that calls the callback function. The examples above show that a request must be made before the callback is triggered).
The callback function was chosen to be a placeholder instead of handling the actual processing because any API call made within it would instantly crash the component. So, by segmenting functionality into different functions with one procedure that handles coordination, API calls can be made without encountering fatal errors.
Conclusion
The procedure might seem simplistic. But without understanding how callbacks and function pointers operate within Visual Basic, this task can become a challenge very quickly. The key in this process is that functionality is segmented into different procedures with one central location where those functions are brought together. The impression was given that the SnmpMgrRequest function, which is a part of the SNMP Management API (mgmtapi.lib) that was demonstrated in the original NetMgr SNMP component, acts in a very similar fashion. This methodology can be applied to other API calls that allow or force the use of callbacks. Please feel free to email bgarcia@adaptiveinnovations.net with any questions about this convention.
All references provided dealt primarily with SNMP and the NetMgr components. This wasn't meant to be an additional tutorial on either of these items. The NetMgrVB component demonstrated the issue and a work-around for dealing with function pointers in a Visual Basic COM environment.
References
MSDN, AddressOf Operator
Q198607 - PRB: Access Violation in VB Run-Time Using AddressOf, Microsoft Knowledge Base
Creating a SNMP Component -- Part 2, by Benjamin Garcia
About the Author
Ben Garcia is the President and Co-founder of Adaptive Innovations, LLC, a solution provider specializing in integration and artificial intelligence. He has more than 13 years of software development experience in many different facets of the industry. Please feel free to contact Ben at bgarcia@adaptiveinnovations.net.
|