Monday 28 January 2019

VBA - Accessibility Meta Code

So I had some code to generate an illustrative tree of Excel window handles as a by-product for some code to get a pointer to Excel via its window handle using the Accessibility API. On StackOverflow, a questioner wanted something similar for Word. One can also imagine a future question wanted similar for PowerPoint. Code to reach all those three applications via their windows handles are in the previous post. However, I wanted to share the meta code because yet another use case may turn up in the future.

Option Explicit
Option Private Module

'******************************************************************************************************
'* Windows API function to traverse windows and grab attributes
'******************************************************************************************************
Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" _
(ByVal hWnd1 As Long, ByVal hwnd2 As Long, ByVal lpsz1 As String, ByVal lpsz2 As String) As Long

Private Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
(ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long

Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" _
(ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long

'******************************************************************************************************
'* Windows API function to get IDispatch COM pointer via Accessibility interface
'******************************************************************************************************
Private Declare PtrSafe Function AccessibleObjectFromWindow Lib "oleacc.dll" ( _
  ByVal hwnd As LongPtr, ByVal dwId As Long, ByRef riid As Any, ByRef ppvObject As Object) As Long

'******************************************************************************************************
'* Entry Points
'******************************************************************************************************
Private Sub TestGetAccessibleOfficeObjects()
    Dim shtExcelReport As Excel.Worksheet
    Set shtExcelReport = ThisWorkbook.Worksheets.Item("Sheet2")
    Dim shtWordReport As Excel.Worksheet
    Set shtWordReport = ThisWorkbook.Worksheets.Item("Sheet3")
    
    Dim shtPowerPointReport As Excel.Worksheet
    Set shtPowerPointReport = ThisWorkbook.Worksheets.Item("Sheet4")
    
    'Dim shtOneNoteReport As Excel.Worksheet
    'Set shtOneNoteReport = ThisWorkbook.Worksheets.Item("Sheet5")
    
    
    GetAccessibleOfficeObjects shtExcelReport, "Excel"
    GetAccessibleOfficeObjects shtWordReport, "Word"
    GetAccessibleOfficeObjects shtPowerPointReport, "PowerPoint"
    'OneNote not accessible GetAccessibleOfficeObjects shtOneNoteReport, "OneNote"

End Sub

'******************************************************************************************************
'******************************************************************************************************
'* Main Controlling code
'******************************************************************************************************
'******************************************************************************************************
Private Function GetAccessibleOfficeObjects(ByVal shOutputSheet As Excel.Worksheet, ByVal sApplication As String)

    Dim sApplicationRootClass As String
    
    sApplicationRootClass = VBA.IIf(sApplication = "Excel", "XLMAIN", VBA.IIf(sApplication = "Word", "OpusApp", "Not yet implemented"))
    sApplicationRootClass = VBA.Switch(sApplication = "Excel", "XLMAIN", sApplication = "Word", "OpusApp", _
                sApplication = "PowerPoint", "PPTFrameClass", sApplication = "OneNote", "Framework::CFrame")
    
    Dim xmlHandlesReport As MSXML2.DOMDocument60
    Set xmlHandlesReport = GetWindows(sApplicationRootClass, True, shOutputSheet)
    
    WriteToSheetStart shOutputSheet, xmlHandlesReport.DocumentElement
    
    QueryInterfaceForAccessibility xmlHandlesReport
    
    UpdateCastableHandles xmlHandlesReport
     
    WriteCodeToGetAppObject xmlHandlesReport, sApplication, sApplicationRootClass
End Function

Private Function WriteCodeToGetAppObject(ByVal xmlHandlesReport As MSXML2.DOMDocument60, _
            ByVal sApplication As String, ByVal sApplicationRootClass As String)

    Dim xmlAppNode As MSXML2.IXMLDOMElement
    Set xmlAppNode = xmlHandlesReport.SelectSingleNode("//*[@obj_Application_Name]")
        
    '******************************************************************************************************
    '* Traverse up the tree to the root node, recording the window class
    '******************************************************************************************************
    Dim dicPathUp As Scripting.Dictionary
    Set dicPathUp = New Scripting.Dictionary
    
    Dim xmlTraverser As MSXML2.IXMLDOMElement
    Set xmlTraverser = xmlAppNode
    
    While Not xmlTraverser Is xmlHandlesReport.DocumentElement
        dicPathUp.Add xmlTraverser.getAttribute("class"), 0
        Set xmlTraverser = xmlTraverser.ParentNode
    Wend
    
    '******************************************************************************************************
    '* Reverse the path
    '******************************************************************************************************
    Dim dicPathDown As Scripting.Dictionary
    Set dicPathDown = New Scripting.Dictionary
    Dim lLoop As Long, lLoop2 As Long
    For lLoop = dicPathUp.Count - 1 To 0 Step -1
        
        dicPathDown.Add dicPathUp.Keys()(lLoop), 0
    Next lLoop
    
    '******************************************************************************************************
    '* Now write the code
    '******************************************************************************************************
    Dim dicCode As Scripting.Dictionary
    Set dicCode = New Scripting.Dictionary
    
    Dim sFuncName As String
    sFuncName = "Get" & sApplication & "AppObjectByIAccessible"
    
    dicCode.Add dicCode.Count, "Private Declare PtrSafe Function AccessibleObjectFromWindow Lib ""oleacc.dll"" ( _"
    dicCode.Add dicCode.Count, "    ByVal hwnd As LongPtr, ByVal dwId As Long, ByRef riid As Any, ByRef ppvObject As Object) As Long" & vbNewLine

    dicCode.Add dicCode.Count, "Private Declare Function FindWindowEx Lib ""user32"" Alias ""FindWindowExA"" _"
    dicCode.Add dicCode.Count, "    (ByVal hWnd1 As Long, ByVal hwnd2 As Long, ByVal lpsz1 As String, ByVal lpsz2 As String) As Long" & vbNewLine
    
    dicCode.Add dicCode.Count, "Public Function " & sFuncName & "() As Object"
    dicCode.Add dicCode.Count, "    Dim guid(0 To 3) As Long, acc As Object"
    dicCode.Add dicCode.Count, "    guid(0) = &H20400: guid(1) = &H0: guid(2) = &HC0: guid(3) = &H46000000"
    dicCode.Add dicCode.Count, ""
    dicCode.Add dicCode.Count, "    Dim alHandles(0 to " & dicPathUp.Count & ") As Long"
    
    dicCode.Add dicCode.Count, "    alHandles(0) = FindWindowEx(0, 0, """ & sApplicationRootClass & """, vbNullString)"
    For lLoop = 0 To dicPathDown.Count - 1
        Dim sInputHandle As String
        If lLoop = 0 Then sInputHandle = "lTopHandle" Else sInputHandle = "alHandles(" & lLoop & ")"
        sInputHandle = "alHandles(" & lLoop & ")"
        
        Dim sClass As String
        sClass = dicPathDown.Keys()(lLoop)
        dicCode.Add dicCode.Count, "    alHandles(" & lLoop + 1 & ") = FindWindowEx(" & sInputHandle & ",0,""" & sClass & """,vbNullString) "
    Next
    
    dicCode.Add dicCode.Count, "    If AccessibleObjectFromWindow(alHandles(" & dicPathDown.Count & "), -16&, guid(0), acc) = 0 Then"
    dicCode.Add dicCode.Count, "        Set " & sFuncName & " = acc.Application"
    dicCode.Add dicCode.Count, "    End If"
    dicCode.Add dicCode.Count, "End Function " & vbNewLine
    dicCode.Add dicCode.Count, "Sub Test" & sFuncName
    dicCode.Add dicCode.Count, "    Dim obj As Object"
    dicCode.Add dicCode.Count, "    Set obj = " & sFuncName & "()"
    dicCode.Add dicCode.Count, "    Debug.Print obj.Name"
    dicCode.Add dicCode.Count, "End Sub "
    
    Debug.Print VBA.Join(dicCode.Items, vbNewLine)
End Function

'******************************************************************************************************
'* Global functions
'******************************************************************************************************
Public Function PadHex(ByVal l32Bit As Long) As String
    PadHex = Right$("00000000" & Hex$(l32Bit), 8)
End Function

'******************************************************************************************************
'******************************************************************************************************
'* Windows Recursion Code
'******************************************************************************************************
'******************************************************************************************************
Private Function GetWindows(ByVal sRootClass As String, ByVal bWriteToSheet As Boolean, ByVal ws As Excel.Worksheet) As MSXML2.DOMDocument60
    Debug.Assert Not ws Is Nothing
    
    Dim xmlDocument As MSXML2.DOMDocument60
    Set xmlDocument = New MSXML2.DOMDocument60
    xmlDocument.LoadXML ""
    Debug.Assert xmlDocument.parseError = 0
    
    Dim xmlRoot As MSXML2.IXMLDOMElement
    Set xmlRoot = xmlDocument.DocumentElement
     
    Dim hwnd As Long
    hwnd = FindWindowEx(0, 0, sRootClass, vbNullString)
    
    AdornAttributes hwnd, xmlRoot
    GetWinInfo hwnd&, xmlRoot
    
    Set GetWindows = xmlDocument
End Function

Private Function AdornAttributes(ByVal hwnd As Long, xmlEle As MSXML2.IXMLDOMElement)
    If Not xmlEle Is Nothing Then
        Call xmlEle.setAttribute("hWndHex", PadHex(hwnd))
        Call xmlEle.setAttribute("hWnd", hwnd)
        Call xmlEle.setAttribute("title", GetTitle(hwnd))
        Call xmlEle.setAttribute("class", GetClassName2(hwnd))
    End If
End Function

Private Sub GetWinInfo(hParent As Long, ByVal xmlEle As MSXML2.IXMLDOMElement)
    '* Sub to recursively obtain window handles, classes and text
    '* given a parent window to search
    '* Based on code written by Mark Rowlinson - www.markrowlinson.co.uk - The Programming Emporium
    '* modified to write to Xml document instead of a worksheet
    Dim hwnd As Long
    
    hwnd = FindWindowEx(hParent, 0&, vbNullString, vbNullString)
    While hwnd <> 0
        
        Dim xmlChild As MSXML2.IXMLDOMElement
        Set xmlChild = xmlEle.OwnerDocument.createElement("Window")
        xmlEle.appendChild xmlChild
        
        AdornAttributes hwnd, xmlChild
        
        GetWinInfo hwnd, xmlChild
        
        hwnd = FindWindowEx(hParent, hwnd, vbNullString, vbNullString)
    Wend
End Sub

Private Function GetTitle(ByVal hwnd As Long, Optional ByVal bReportNa As Boolean) As String
    Dim lngRet As Long
    Dim strText As String
    
    strText = String$(100, Chr$(0))
    lngRet = GetWindowText(hwnd, strText, 100)
    If lngRet > 0 Then
        GetTitle = Left$(strText, lngRet)
    Else
        If bReportNa Then
            GetTitle = "N/A"
        End If
    End If
End Function

Private Function GetClassName2(ByVal hwnd As Long)
    Dim lngRet As Long
    Dim strText As String
    
    strText = String$(100, Chr$(0))
    lngRet = GetClassName(hwnd, strText, 100)
    GetClassName2 = Left$(strText, lngRet)
End Function

'******************************************************************************************************
'******************************************************************************************************
'* Write Report To Worksheet code
'******************************************************************************************************
'******************************************************************************************************
Private Function WriteToSheetStart(ByVal ws As Excel.Worksheet, ByVal xmlEle As MSXML2.IXMLDOMElement)
    ws.Cells.Clear
    ws.Activate
    ws.Cells(1, 1).Activate
    WriteToSheet ws, 1, 1, xmlEle
End Function

Private Function WriteToSheet(ByVal ws As Excel.Worksheet, ByVal lRow As Long, ByVal lColumn As Long, ByVal xmlEle As MSXML2.IXMLDOMElement) As Long
    
    Dim hwnd As Long
    hwnd = xmlEle.getAttribute("hWnd")
    
    
    Call xmlEle.setAttribute("rangeAnchor", "'" & ws.Name & "'!" & ws.Cells(lRow, lColumn).AddressLocal)
    Dim rngTest As Excel.Range
    Set rngTest = Application.Range(xmlEle.getAttribute("rangeAnchor"))
    Debug.Assert Not rngTest Is Nothing
    
    
    ws.Cells(lRow, lColumn).Formula = "'" & PadHex(hwnd) & " (" & CStr(hwnd) & ")"
    ws.Cells(lRow, lColumn + 1).Value = xmlEle.getAttribute("title")
    ws.Cells(lRow, lColumn + 2).Value = xmlEle.getAttribute("class")
    
    If xmlEle.ChildNodes.Length > 0 Then
    
        Dim xmlChildWindows As MSXML2.IXMLDOMNodeList
        Set xmlChildWindows = xmlEle.ChildNodes
        
        Dim lLength As Long
        lLength = xmlEle.ChildNodes.Length
        
        Dim lLoop As Long
        For lLoop = 0 To lLength - 1
            
            Dim xmlChild As MSXML2.IXMLDOMElement
            Set xmlChild = xmlEle.ChildNodes.Item(lLoop)
    
            lRow = WriteToSheet(ws, lRow + 1, lColumn + 3, xmlChild)
            
        Next lLoop
    End If
    
    WriteToSheet = lRow
End Function

Private Sub UpdateCastableHandles(ByVal xmlHandlesReport As MSXML2.DOMDocument60)
    Dim rngCastable As Excel.Range
    
    Dim xmlCastable As MSXML2.IXMLDOMNodeList
    Set xmlCastable = xmlHandlesReport.SelectNodes("//*[@ComInterface]")
    Debug.Assert xmlCastable.Length > 0
    
    Dim xmlCastableLoop As MSXML2.IXMLDOMElement
    For Each xmlCastableLoop In xmlCastable
        Set rngCastable = Application.Range(xmlCastableLoop.getAttribute("rangeAnchor"))
        rngCastable.Resize(1, 3).Interior.Color = rgbYellow
        
        rngCastable.EntireColumn.AutoFit
        rngCastable.Offset(0, 1).EntireColumn.AutoFit
        rngCastable.Offset(0, 2).EntireColumn.AutoFit
        
'        Dim rngAutoFitLoop As Excel.Range
'        For Each rngAutoFitLoop In rngCastable.Cells
'            rngAutoFitLoop.EntireColumn.AutoFit
'        Next
        
        rngCastable.AddComment
        rngCastable.Comment.Visible = True
        rngCastable.Comment.Text Text:=xmlCastableLoop.getAttribute("ComInterface")
        
        Dim shp As Shape
        Set shp = rngCastable.Comment.Shape
        
        shp.ScaleWidth 1.07, msoFalse, msoScaleFromTopLeft
        shp.ScaleHeight 0.29, msoFalse, msoScaleFromTopLeft
        shp.IncrementTop -20
        
    Next
    
    Dim xmlApp As MSXML2.IXMLDOMElement
    Set xmlApp = xmlHandlesReport.SelectSingleNode("//*[@obj_Application_Name]")
    
    Debug.Assert Not xmlApp Is Nothing

    Dim rngApp As Excel.Range
    Set rngApp = Application.Range(xmlApp.getAttribute("rangeAnchor"))
    rngApp.Resize(1, 3).Interior.Color = rgbLightGreen
    Debug.Assert Not rngApp Is Nothing
End Sub

'******************************************************************************************************
'******************************************************************************************************
'* Accessibility QueryInterface code
'******************************************************************************************************
'******************************************************************************************************
Private Sub QueryInterfaceForAccessibility(ByVal xmlHandlesReport As MSXML2.DOMDocument60)

    Dim dicHandles As Scripting.Dictionary
    Set dicHandles = AllHandles(xmlHandlesReport.DocumentElement)

    Dim guid(0 To 3) As Long, acc As Object
    guid(0) = &H20400
    guid(1) = &H0
    guid(2) = &HC0
    guid(3) = &H46000000
    
    Dim vHandleLoop As Variant
    For Each vHandleLoop In dicHandles.Keys
        
        Dim xmlNode As MSXML2.IXMLDOMElement
        Set xmlNode = xmlHandlesReport.SelectSingleNode("//Window[@hWnd='" & vHandleLoop & "']")
        
        If Not xmlNode Is Nothing Then
        
            Set acc = Nothing
            
            DoEvents
            Dim lReturnIDisp As Long
            lReturnIDisp = AccessibleObjectFromWindow(vHandleLoop, -16&, guid(0), acc)
            DoEvents
    
            If lReturnIDisp = 0 Then
                Call xmlNode.setAttribute("ComInterface", TypeName(acc))
                
                'Debug.Print PadHex(vHandleLoop), TypeName(acc)
                
                Dim sAppName As String
                sAppName = ""
                
                Dim objApp As Object
                Set objApp = Nothing
                On Error Resume Next
                sAppName = acc.Application.Name
                On Error GoTo 0
                If Len(sAppName) > 0 Then
                    Call xmlNode.setAttribute("obj_Application_Name", sAppName)
                End If
            End If
           
        End If
        Set acc = Nothing
    Next

End Sub

Private Function AllHandles(ByVal xmlEle As MSXML2.IXMLDOMElement) As Scripting.Dictionary
    
    Dim dicHandles As Scripting.Dictionary
    Set dicHandles = New Scripting.Dictionary

    Dim xmlHandlesList As MSXML2.IXMLDOMNodeList
    Set xmlHandlesList = xmlEle.SelectNodes("//*[@hWnd]")

    Dim vLoop As MSXML2.IXMLDOMElement
    For Each vLoop In xmlHandlesList
        dicHandles.Add CLng(vLoop.getAttribute("hWnd")), 0
    Next

    Set AllHandles = dicHandles
End Function

VBA - Code to get Excel, Word, PowerPoint from window handle

The following code shows three Office applications accessible from their windows handle (please ensure you have them running before testing). They work via the Accessibility API. I had some fun writing code to find the general pattern and that generic code follows in the next post.

  1. Option Explicit
  2. Option Private Module
  3.  
  4. Private Declare PtrSafe Function AccessibleObjectFromWindow Lib "oleacc.dll" ( _
  5.     ByVal hwnd As LongPtr, ByVal dwId As LongByRef riid As Any, ByRef ppvObject As ObjectAs Long
  6.  
  7. Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" _
  8.     (ByVal hWnd1 As LongByVal hwnd2 As LongByVal lpsz1 As StringByVal lpsz2 As StringAs Long
  9.  
  10. Public Function GetExcelAppObjectByIAccessible() As Object
  11.     Dim guid(0 To 3) As Long, acc As Object
  12.     guid(0) = &H20400 : guid(1) = &H0 : guid(2) = &HC0 : guid(3) = &H46000000
  13.  
  14.     Dim alHandles(0 To 2) As Long
  15.     alHandles(0) = FindWindowEx(0, 0, "XLMAIN", vbNullString)
  16.     alHandles(1) = FindWindowEx(alHandles(0), 0, "XLDESK", vbNullString)
  17.     alHandles(2) = FindWindowEx(alHandles(1), 0, "EXCEL7", vbNullString)
  18.     If AccessibleObjectFromWindow(alHandles(2), -16&, guid(0), acc) = 0 Then
  19.         Set GetExcelAppObjectByIAccessible = acc.Application
  20.     End If
  21. End Function
  22.  
  23.  
  24. Public Function GetWordAppObjectByIAccessible() As Object
  25.     Dim guid(0 To 3) As Long, acc As Object
  26.     guid(0) = &H20400 : guid(1) = &H0 : guid(2) = &HC0 : guid(3) = &H46000000
  27.  
  28.     Dim alHandles(0 To 3) As Long
  29.     alHandles(0) = FindWindowEx(0, 0, "OpusApp", vbNullString)
  30.     alHandles(1) = FindWindowEx(alHandles(0), 0, "_WwF", vbNullString)
  31.     alHandles(2) = FindWindowEx(alHandles(1), 0, "_WwB", vbNullString)
  32.     alHandles(3) = FindWindowEx(alHandles(2), 0, "_WwG", vbNullString)
  33.     If AccessibleObjectFromWindow(alHandles(3), -16&, guid(0), acc) = 0 Then
  34.         Set GetWordAppObjectByIAccessible = acc.Application
  35.     End If
  36. End Function
  37.  
  38.  
  39. Public Function GetPowerPointAppObjectByIAccessible() As Object
  40.     Dim guid(0 To 3) As Long, acc As Object
  41.     guid(0) = &H20400 : guid(1) = &H0 : guid(2) = &HC0 : guid(3) = &H46000000
  42.  
  43.     Dim alHandles(0 To 2) As Long
  44.     alHandles(0) = FindWindowEx(0, 0, "PPTFrameClass", vbNullString)
  45.     alHandles(1) = FindWindowEx(alHandles(0), 0, "MDIClient", vbNullString)
  46.     alHandles(2) = FindWindowEx(alHandles(1), 0, "mdiClass", vbNullString)
  47.     If AccessibleObjectFromWindow(alHandles(2), -16&, guid(0), acc) = 0 Then
  48.         Set GetPowerPointAppObjectByIAccessible = acc.Application
  49.     End If
  50. End Function
  51.  
  52. Sub TestGetExcelAppObjectByIAccessible()
  53.     Dim obj As Object
  54.     Set obj = GetExcelAppObjectByIAccessible()
  55.     Debug.Print obj.Name
  56. End Sub
  57.  
  58.  
  59. Sub TestGetWordAppObjectByIAccessible()
  60.     Dim obj As Object
  61.     Set obj = GetWordAppObjectByIAccessible()
  62.     Debug.Print obj.Name
  63. End Sub
  64.  
  65.  
  66. Sub TestGetPowerPointAppObjectByIAccessible()
  67.     Dim obj As Object
  68.     Set obj = GetPowerPointAppObjectByIAccessible()
  69.     Debug.Print obj.Name
  70. End Sub
  71.  
  72.  
  73.  
  74.  

We can visualise what is going on with the following output reports, first the Excel windows handles report ...

Next the Word windows handles report ...

Finally the PowerPoint windows handles report ...

Tuesday 22 January 2019

VBA - Sockets - Ruby - Java - Interop Nirvana with Sockets!

I hope you have noticed that inter-operability features large on this blog. Interop is easy with C# because it has an excellent COM interop library. Python and other languages also have COM APIs (not always as comprehensive as C#). But Java and Ruby do not have COM APIs callable from VBA. But this month we have shown that it is possible to use Sockets to connect with other processes (in the demos on the same machine but in real life potentially on remote machines). Thus, using sockets Excel VBA call anything, literally anything (so long the target langauge has a sockets API).

In the previous two articles I wrote sockets servers in Ruby then Java which ran simple calculations. They were just warm-up and test-beds to be honest. What I really wanted to show was VBA calling Ruby and Java.

So in the code below if you run the procedure TestWS2SendAndReceive() at line 159 then you can see the code will attempt to call the Java and Ruby servers, make sure you have these running. If all works then you should get output in the Immediate window

1+3 = 4.0
6*7 = 42.0

modVBASocketsClient Standard Module

  1. Option Explicit
  2.  
  3. Option Private Module
  4.  
  5.  
  6. 'reference Windows Sockets 2 - Windows applications _ Microsoft Docs
  7. 'http://msdn.microsoft.com/en-us/library/windows/desktop/ms740673(v=vs.85).aspx
  8. Private Const INVALID_SOCKET = -1
  9. Private Const WSADESCRIPTION_LEN = 256
  10. Private Const SOCKET_ERROR As Long = -1 'const #define SOCKET_ERROR            (-1)
  11.  
  12. Private Enum AF
  13.     AF_UNSPEC = 0
  14.     AF_INET = 2
  15.     AF_IPX = 6
  16.     AF_APPLETALK = 16
  17.     AF_NETBIOS = 17
  18.     AF_INET6 = 23
  19.     AF_IRDA = 26
  20.     AF_BTH = 32
  21. End Enum
  22.  
  23. Private Enum sock_type
  24.     SOCK_STREAM = 1
  25.     SOCK_DGRAM = 2
  26.     SOCK_RAW = 3
  27.     SOCK_RDM = 4
  28.     SOCK_SEQPACKET = 5
  29. End Enum
  30.  
  31. Private Enum Protocol
  32.     IPPROTO_ICMP = 1
  33.     IPPROTO_IGMP = 2
  34.     BTHPROTO_RFCOMM = 3
  35.     IPPROTO_TCP = 6
  36.     IPPROTO_UDP = 17
  37.     IPPROTO_ICMPV6 = 58
  38.     IPPROTO_RM = 113
  39. End Enum
  40.  
  41. 'Private Type sockaddr
  42. '    sa_family As Integer
  43. '    sa_data(0 To 13) As Byte
  44. 'End Type
  45.  
  46. Private Type sockaddr_in
  47.     sin_family As Integer
  48.     sin_port(0 To 1) As Byte
  49.     sin_addr(0 To 3) As Byte
  50.     sin_zero(0 To 7) As Byte
  51. End Type
  52.  
  53.  
  54. 'typedef UINT_PTR        SOCKET;
  55. Private Type udtSOCKET
  56.     pointer As Long
  57. End Type
  58.  
  59.  
  60.  
  61. ' typedef struct WSAData {
  62. '  WORD           wVersion;
  63. '  WORD           wHighVersion;
  64. '  char           szDescription[WSADESCRIPTION_LEN+1];
  65. '  char           szSystemStatus[WSASYS_STATUS_LEN+1];
  66. '  unsigned short iMaxSockets;
  67. '  unsigned short iMaxUdpDg;
  68. '  char FAR       *lpVendorInfo;
  69. '} WSADATA, *LPWSADATA;
  70.  
  71. Private Type udtWSADATA
  72.     wVersion As Integer
  73.     wHighVersion As Integer
  74.     szDescription(0 To WSADESCRIPTION_LEN) As Byte
  75.     szSystemStatus(0 To WSADESCRIPTION_LEN) As Byte
  76.     iMaxSockets As Integer
  77.     iMaxUdpDg As Integer
  78.     lpVendorInfo As Long
  79. End Type
  80.  
  81. 'int errorno = WSAGetLastError()
  82. Private Declare Function WSAGetLastError Lib "Ws2_32" () As Integer
  83.  
  84. '   int WSAStartup(
  85. '  __in   WORD wVersionRequested,
  86. '  __out  LPWSADATA lpWSAData
  87. ');
  88. Private Declare Function WSAStartup Lib "Ws2_32" _
  89.     (ByVal wVersionRequested As IntegerByRef lpWSAData As udtWSADATA) As Long 'winsockErrorCodes2
  90.  
  91.  
  92. '    SOCKET WSAAPI socket(
  93. '  __in  int af,
  94. '  __in  int type,
  95. '  __in  int protocol
  96. ');
  97.  
  98. Private Declare Function ws2_socket Lib "Ws2_32" Alias "socket" _
  99.     (ByVal AF As LongByVal stype As LongByVal Protocol As LongAs LongPtr
  100.  
  101. Private Declare Function ws2_closesocket Lib "Ws2_32" Alias "closesocket" _
  102.     (ByVal socket As LongAs Long
  103.  
  104. 'int recv(
  105. '  SOCKET s,
  106. '  char   *buf,
  107. '  int    len,
  108. '  int    flags
  109. ');
  110. Private Declare Function ws2_recv Lib "Ws2_32" Alias "recv" _
  111.     (ByVal socket As LongByVal buf As LongPtr,
  112.      ByVal length As LongByVal flags As LongAs Long
  113.  
  114. 'int WSAAPI connect(
  115. '  SOCKET         s,
  116. '  const sockaddr *name,
  117. '  int            namelen
  118. ');
  119.  
  120. Private Declare Function ws2_connect Lib "Ws2_32" Alias "connect" _
  121.     (ByVal As LongPtr, ByRef name As sockaddr_in, ByVal namelen As LongAs Long
  122.  
  123. 'int WSAAPI send(
  124. '  SOCKET     s,
  125. '  const char *buf,
  126. '  int        len,
  127. '  int        flags
  128. ');
  129. Private Declare Function ws2_send Lib "Ws2_32" Alias "send" _
  130.     (ByVal As LongPtr, ByVal buf As LongPtr, ByVal buflen As LongByVal flags As LongAs Long
  131.  
  132.  
  133. Private Declare Function ws2_shutdown Lib "Ws2_32" Alias "shutdown" _
  134.         (ByVal As LongByVal how As LongAs Long
  135.  
  136. Private Declare Sub WSACleanup Lib "Ws2_32" ()
  137.  
  138. Private Enum eShutdownConstants
  139.     SD_RECEIVE = 0  '#define SD_RECEIVE      0x00
  140.     SD_SEND = 1     '#define SD_SEND         0x01
  141.     SD_BOTH = 2     '#define SD_BOTH         0x02
  142. End Enum
  143.  
  144. Sub TestPortLongToBytes()
  145.     'redis is on port number 6379
  146.     Dim abytPortAsBytes() As Byte
  147.     abytPortAsBytes() = PortLongToBytes(6379)
  148.     Debug.Assert abytPortAsBytes(0) = 24
  149.     Debug.Assert abytPortAsBytes(1) = 235
  150. End Sub
  151.  
  152. Private Function PortLongToBytes(ByVal lPort As IntegerAs Byte()
  153.     ReDim abytReturn(0 To 1) As Byte
  154.     abytReturn(0) = lPort \ 256
  155.     abytReturn(1) = lPort Mod 256
  156.     PortLongToBytes = abytReturn()
  157. End Function
  158.  
  159. Private Sub TestWS2SendAndReceive()
  160.  
  161.     Dim sResponse As String
  162.     Const clJavaPort As Long = 6666
  163.     Const clRubyPort As Long = 3000
  164.     If WS2SendAndReceive(clJavaPort, "1+3" & vbCrLf, sResponse) Then
  165.         Debug.Print sResponse
  166.     End If
  167.     If WS2SendAndReceive(clRubyPort, "6*7" & vbCrLf, sResponse) Then
  168.         Debug.Print sResponse
  169.     End If
  170. End Sub
  171.  
  172.  
  173. Public Function WS2SendAndReceive(ByVal lPort As Long,
  174.             ByVal sText As StringByRef psResponse As StringAs Boolean
  175.     'https://docs.microsoft.com/en-gb/windows/desktop/api/winsock/nf-winsock-recv
  176.     If Right$(sText, 2) <> vbCrLf Then Err.Raise vbObjectError, , "Best suffix your sends with a new line (vbCrLf)"
  177.     psResponse = ""
  178.     '//----------------------
  179.     '// Declare and initialize variables.
  180.     Dim iResult As Integer : iResult = 0
  181.     Dim wsaData As udtWSADATA
  182.  
  183.     Dim ConnectSocket As LongPtr
  184.  
  185.     Dim clientService As sockaddr_in
  186.  
  187.     Dim sendBuf() As Byte
  188.     sendBuf = StrConv(sText, vbFromUnicode)
  189.  
  190.     Const recvbuflen As Long = 512
  191.     Dim recvbuf(0 To recvbuflen - 1) As Byte
  192.  
  193.     '//----------------------
  194.     '// Initialize Winsock
  195.     Dim eResult As Long 'winsockErrorCodes2
  196.     eResult = WSAStartup(&H202, wsaData)
  197.     If eResult <> 0 Then
  198.         Debug.Print "WSAStartup failed with error: " & eResult
  199.         WS2SendAndReceive = False
  200.         GoTo SingleExit
  201.     End If
  202.  
  203.  
  204.     '//----------------------
  205.     '// Create a SOCKET for connecting to server
  206.     ConnectSocket = ws2_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
  207.     If ConnectSocket = INVALID_SOCKET Then
  208.         Dim eLastError As Long 'winsockErrorCodes2
  209.         eLastError = WSAGetLastError()
  210.         Debug.Print "socket failed with error: " & eLastError
  211.         Call WSACleanup
  212.         WS2SendAndReceive = False
  213.         GoTo SingleExit
  214.     End If
  215.  
  216.  
  217.     '//----------------------
  218.     '// The sockaddr_in structure specifies the address family,
  219.     '// IP address, and port of the server to be connected to.
  220.     clientService.sin_family = AF_INET
  221.  
  222.     clientService.sin_addr(0) = 127
  223.     clientService.sin_addr(1) = 0
  224.     clientService.sin_addr(2) = 0
  225.     clientService.sin_addr(3) = 1
  226.  
  227.     Dim abytPortAsBytes() As Byte
  228.     abytPortAsBytes() = PortLongToBytes(lPort)
  229.  
  230.     clientService.sin_port(1) = 235  '* 6379
  231.     clientService.sin_port(0) = 24
  232.  
  233.     clientService.sin_port(1) = abytPortAsBytes(1)
  234.     clientService.sin_port(0) = abytPortAsBytes(0)
  235.  
  236.     '//----------------------
  237.     '// Connect to server.
  238.  
  239.     iResult = ws2_connect(ConnectSocket, clientService, LenB(clientService))
  240.     If (iResult = SOCKET_ERROR) Then
  241.  
  242.         eLastError = WSAGetLastError()
  243.  
  244.         Debug.Print "connect failed with error: " & eLastError
  245.         Call ws2_closesocket(ConnectSocket)
  246.         Call WSACleanup
  247.         WS2SendAndReceive = False
  248.         GoTo SingleExit
  249.     End If
  250.  
  251.     '//----------------------
  252.     '// Send an initial buffer
  253.     Dim sendbuflen As Long
  254.     sendbuflen = UBound(sendBuf) - LBound(sendBuf) + 1
  255.     iResult = ws2_send(ConnectSocket, VarPtr(sendBuf(0)), sendbuflen, 0)
  256.     If (iResult = SOCKET_ERROR) Then
  257.         eLastError = WSAGetLastError()
  258.         Debug.Print "send failed with error: " & eLastError
  259.  
  260.         Call ws2_closesocket(ConnectSocket)
  261.         Call WSACleanup
  262.         WS2SendAndReceive = False
  263.         GoTo SingleExit
  264.     End If
  265.  
  266.     'Debug.Print "Bytes Sent: ", iResult
  267.  
  268.     '// shutdown the connection since no more data will be sent
  269.     iResult = ws2_shutdown(ConnectSocket, SD_SEND)
  270.     If (iResult = SOCKET_ERROR) Then
  271.  
  272.         eLastError = WSAGetLastError()
  273.         Debug.Print "shutdown failed with error: " & eLastError
  274.  
  275.         Call ws2_closesocket(ConnectSocket)
  276.         Call WSACleanup
  277.         WS2SendAndReceive = False
  278.         GoTo SingleExit
  279.     End If
  280.  
  281.     ' receive only one message (TODO handle when buffer is not large enough)
  282.  
  283.     iResult = ws2_recv(ConnectSocket, VarPtr(recvbuf(0)), recvbuflen, 0)
  284.     If (iResult > 0) Then
  285.         'Debug.Print "Bytes received: ", iResult
  286.     ElseIf (iResult = 0) Then
  287.         Debug.Print "Connection closed"
  288.         WS2SendAndReceive = False
  289.         Call ws2_closesocket(ConnectSocket)
  290.         Call WSACleanup
  291.         GoTo SingleExit
  292.     Else
  293.         eLastError = WSAGetLastError()
  294.         Debug.Print "recv failed with error: " & eLastError
  295.     End If
  296.  
  297.     psResponse = Left$(StrConv(recvbuf, vbUnicode), iResult)
  298.  
  299.     'Debug.Print psResponse
  300.  
  301.     '// close the socket
  302.     iResult = ws2_closesocket(ConnectSocket)
  303.     If (iResult = SOCKET_ERROR) Then
  304.  
  305.         eLastError = WSAGetLastError()
  306.         Debug.Print "close failed with error: " & eLastError
  307.  
  308.         Call WSACleanup
  309.         WS2SendAndReceive = False
  310.         GoTo SingleExit
  311.     End If
  312.  
  313.     Call WSACleanup
  314.     WS2SendAndReceive = True
  315.  
  316. SingleExit:
  317.     Exit Function
  318. ErrHand:
  319.  
  320. End Function

Sockets - Java to Java - Sockets programming

Like most great languages Java has the ability open and read/write from/to sockets, a TCP/IP programming protocol lower than HTTP. This month I have been demonstrating Excel VBA acting as one endpoint for a Sockets connection (specifically communicating to Redis). That Excel VBA can talk to sockets means we can get VBA to talk to Java. I will write an VBA to Java article shortly but first it is better to write a Java to Java sockets application to see what is involved.

(And the more attentive amongst you will spot that I have just written a Ruby to Ruby sockets application so let's see if its true than Java is more verbose.)

The following code is a simple single-thread app (if you want multi-threaded, see the original article as we are here to demonstrate the sockets only.

JavaSocketsCalcClient.java

So this is the client code which simply reads a line of input in the form of a sum like "1+3" (actually you can do subtraction, divide and multiply), calls the server via sockets and then prints the return.

Here is the code for the file JavaSocketsCalcClient.java

//With thanks to https://www.baeldung.com/a-guide-to-java-sockets
import java.net.*;
import java.io.*;

public class JavaSocketsCalcClient {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String ip, int port) {
        try {
            clientSocket = new Socket(ip, port);
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
        }
    }

    public String sendMessage(String msg) {
        try {
            out.println(msg);
            String resp = in.readLine();
            return resp;
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
            return "";
        }
    }

    public void stopConnection() {
        try {
            in.close();
            out.close();
            clientSocket.close();
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
        }
    }

    public static void main(String[] args) {
        try {
            GreetClient client = new GreetClient();
            client.startConnection("127.0.0.1", 6666);
            InputStreamReader reader = new InputStreamReader(System.in);
            BufferedReader in = new BufferedReader(reader);
            while (true) {
                System.out.println(client.sendMessage(in.readLine()));
            }
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
        }
    }
}

So compile and run you need the following commands (and I'm surprised we do not use the .class suffix in the second to be honest).

N:\java\javaSockets>javac JavaSocketsCalcClient.java
N:\java\javaSockets>java JavaSocketsCalcClient

JavaSocketsCalcServer.java

This is the server which takes the sum string e.g. "1+3" and then parses it with a regular expression before calculating and returning the answer to the client.

//With thanks to https://www.baeldung.com/a-guide-to-java-sockets
import java.net.*;
import java.io.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class JavaSocketsCalcServer {
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void start(int port) {
        try {
            serverSocket = new ServerSocket(port);
            System.out.println("Running on port " + port);

            String pattern = "(\\d+)\\s*(\\+|\\*|-|\\/)\\s*(\\d+)";

            // Create a Pattern object
            Pattern r = Pattern.compile(pattern);

            while (true) {

                clientSocket = serverSocket.accept();
                out = new PrintWriter(clientSocket.getOutputStream(), true);
                in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

                try {
                    System.out.println("waiting to read line ...");
                    String sum = in.readLine();
                    System.out.println("Received text:" + sum);
                    // Now create matcher object.
                    Matcher m = r.matcher(sum);
                    if (m.find()) {
                        Float arg0 = Float.parseFloat(m.group(1));
                        String op = m.group(2);
                        Float arg1 = Float.parseFloat(m.group(3));
                        String resp = "";
                        if (op.equals("+")) {
                            resp = String.valueOf(arg0 + arg1);
                        } else if (op.equals("-")) {
                            resp = String.valueOf(arg0 - arg1);
                        } else if (op.equals("*")) {
                            resp = String.valueOf(arg0 * arg1);
                        } else if (op.equals("/")) {
                            resp = String.valueOf(arg0 / arg1);
                        } else {
                            out.println("could not match operand");
                        }

                        out.println(sum + " = " + resp);
                    } else {
                        out.println("does not look calculable");
                    }
                } catch (Exception exc) {
                    System.out.println(exc.getMessage());
                }
            }
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
        }
    }

    public void stop() {
        try {
            in.close();
            out.close();
            clientSocket.close();
            serverSocket.close();
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
        }
    }

    public static void main(String[] args) {
        GreetServer server = new GreetServer();
        server.start(6666);
    }
}

So in a separate console window, do the following

N:\java\javaSockets>javac JavaSocketsCalcServer.java
N:\java\javaSockets>java JavaSocketsCalcServer

Here is a screenshot of the two consoles running and communicating

Keen observers of the code will realise they both run in endless loops. Never mind about that, I simply wanted to get the servers running and demonstrable.

What's Next?

So what's next is some code which shows VBA calling this Java server and also the Ruby server from the previous article to illustrate VBA can call Java and Ruby despite them not having a COM APIs.

Saturday 19 January 2019

Sockets - Ruby to Ruby - Starting Ruby Sockets programming with a simple calculator app

Like most great languages Ruby has the ability open and read/write from/to sockets, a TCP/IP programming protocol lower than HTTP. This month I have been demonstrating Excel VBA acting as one endpoint for a Sockets connection (specifically communicating to Redis). That Excel VBA can talk to sockets means we can get VBA to talk to Ruby. I will write an VBA to Ruby article shortly but first it is better to write a Ruby to Ruby sockets application to see what is involved.

The application will be a simple calculator app. The client will pass a string such as "2*7" to the server, the server will parse the string into operands and operator, calculate the result and return the answer.

Ruby Development Preparation

I've not done any serious Ruby development. So I give a section which highlights some prep work to get an Ruby development environment up and running. First, a tip to see if Ruby is already installed. After that I will give some screenshots of the Ruby extensions that I installed into Microsoft Visual Studio Code.

How do you know if Ruby is Installed?

So I thought I had installed Ruby some time ago. So just taking my Ruby installation for a spin from the command line...

N:\>ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x64-mingw32]

N:\>gem --version
2.7.6

N:\>mkdir ruby

N:\>cd ruby

N:\ruby>mkdir socketsServer

N:\ruby>cd socketsServer

N:\ruby\socketsServer>notepad "RubyHelloWorld.rb"

N:\ruby\socketsServer>type "RubyHelloWorld.rb"
puts "Hello World"

N:\ruby\socketsServer>ruby  "RubyHelloWorld.rb"
Hello World

This looks installed. Also above I have created some directories because I want to open a folder using Visual Studio Code.

Installing Visual Studio Code Ruby Extensions

I know Visual Studio Code is multi-language in that there is an ecosystem of language extensions. So I was confident in picking VS Code for my source code editor. I just needed some extensions. First up was to find from the list of extensions. To view extensions go to menu View->Extensions (that's intuitive!) and then select this one...

Once installed the extension's page should look like this ...

Then during development I wanted my code to be indented for me so I chose this Ruby Formatter extensions as well one as well.

Sockets Calculator

So I started with the code from this great article Socket Programming in Ruby – Code Like A Girl but the code only sends text messages to and fro. I wanted to up the functionality a little so I wrote a calculator app.

socketServer.rb

So here is the source code for the socketServer.rb

#With thanks to https://code.likeagirl.io/socket-programming-in-ruby-f714131336fd

require "socket"

puts "Starting the Server..................."
server = TCPServer.open(3000) # Server would listen on port 3000
loop { # Servers run forever
  client_connection = server.accept # Establish client connect connection
  begin
    clientText = client_connection.gets.chomp
    puts "clientText:" + clientText
    resp = ""

    #With thanks to https://regex101.com/
    parsed = /(\d+)\s*(\+|\*|-|\/)\s*(\d+)/.match(clientText)
    if parsed
      arg0 = Float(parsed[1])
      op = parsed[2]
      arg1 = Float(parsed[3])
      if op == "+"
        resp = arg0 + arg1
      elsif op == "-"
        resp = arg0 - arg1
      elsif op == "*"
        resp = arg0 * arg1
      elsif op == "/"
        resp = arg0 / arg1
      end
    else
      resp = "does not look calculable"
    end

    client_connection.puts("#{clientText}" + " = " + "#{resp}") # Send the answer to the client
    client_connection.puts("Closing the connection with #{client_connection}")
  rescue Exception => getException
    puts "#{getException}"
  end
  client_connection.close      # Disconnect from the client
}

socketClient.rb

So here is the source code for the socketClient.rb

#With thanks to https://code.likeagirl.io/socket-programming-in-ruby-f714131336fd
require "socket"

while sum = $stdin.gets.chomp # Read lines from the socket
  socket = TCPSocket.open("localhost", 3000)

  #puts "Starting the Client..................."

  socket.puts sum
  while message = socket.gets # Read lines from the socket
    puts message.chomp
  end
  socket.close                    # Close the socket
end

#puts "Closing the Client..................."

Running the program

So run each program in their own console and in the client and then start typing sums ...

Let regular Expressions do the heavy lifting

If you do decide to write a sockets application then you have a lot of string parsing ahead of you. I'd recommend regular expressions. I have found a superb online regular expressions tester, regex101.com. This site gives not just testing panel and quick reference but also it gives an explanation which is a feature that makes this website by far better than any other I have come across. Enjoy!

Friday 18 January 2019

VBA - How to tell if running in Excel or Word

Just a quick post. For a demo I wanted to run some code in Microsoft Word instead of Microsoft Excel. So I needed a method to determine in which environment the code was running. I have selected this code which probes the object model; it uses late binding to slyly evade compile errors.

UPDATE

Ignore code below, Application.Name returns "Microsoft Excel" in Excel and "Microsoft Word" in Word

deprecated...


Function ExcelVBA() As Boolean
    On Error GoTo QuickExit
    Dim objApp As Object
    Set objApp = Application
    
    Dim objWbs As Object
    Set objWbs = objApp.Workbooks
    
    ExcelVBA = True
QuickExit:
End Function

Function WinWordVBA() As Boolean
    On Error GoTo QuickExit
    Dim objApp As Object
    Set objApp = Application
    
    Dim objActiveDocument As Object
    Set objActiveDocument = objApp.ActiveDocument
    
    WinWordVBA = True
QuickExit:
End Function

Thursday 17 January 2019

C# - RTD - Redis - Real Time Data on the worksheet

In this article I share C# code for a Real Time Data worksheet function callable using RTD(). It will call into a local instance of Redis and will tick in real time. This article assumes you have installed Redis.

Click here for separate Youtube window

So I am very much pleased with Redis as outlined in the two articles (here and here) I wrote earlier this month. I hope you have read those previous two articles , and/or watched the videos. Redis is exactly what I have been looking for, I want a machine wide data cache, not too heavyweight but accessible to multiple instances of Excel because it would be inefficient for multiple Excel sessions to draw down duplicate data from network servers. In my humble opinion, much better to draw the data down to one executable, Redis, and then have the Excel sessions call into Redis locally.

RTD Server

There are already examples online for how to write an RTD server in C# (including the superb Learning Tree blog post here) and they share a similar pattern in that they have a timer and the single data item that they return is typically a timestamp so they can show the timestamp moving. I started from those examples and then expanded the functionality. I want multiple data items. I also want to make use of the multiple arguments, for example supplying a JSON path to drill into a JSON document.

Introducing ExcelCoin

I do have a stockbroker but I'm not going to advertise their data. Instead, I invent a fake financial security called ExcelCoin. I write the ExcelCoin price to Redis as a scalar number. I also write a JSON meta document to Redis, which gives the timestamp; for financial securities it could also give trading volume etc. The VBA code for this is given below but it does rely on the C# component given even further below. So just hang on.

I've invented ExcelCoin just to fake a real-time data feed. If you already have a real-time data feed then you can use that to write prices, headlines or whatever to Redis by re-writing the code below.

[If running the code in Word then you need the Python class from this blog post to generate a random Normal observation. If running in Excel then the code uses Excel's WorksheetFunction].

Option Explicit

Private vExcelCoinPrice As Variant

Private moPythonNormsInv As Object
Private moRedisSocket As Object

Public Property Get RedisSocket() As Object
    If moRedisSocket Is Nothing Then
        
        '* see next blog post for codeo this server
        Set moRedisSocket = VBA.CreateObject("RedisRTDServerLib.RedisSocket")
        moRedisSocket.Initialize
    End If
    Set RedisSocket = moRedisSocket
End Property

Public Sub DisposeRedisSocket()
    If Not moRedisSocket Is Nothing Then
        moRedisSocket.Dispose
        Set moRedisSocket = Nothing
    
    End If
End Sub


Public Property Get PythonNormsInv2() As Object
    If moPythonNormsInv Is Nothing Then
        Set moPythonNormsInv = VBA.CreateObject("SciPyInVBA.PythonNormsInv")
    End If
    Set PythonNormsInv2 = moPythonNormsInv
End Property

Sub Test_VBAPyNormStdInv()
    Debug.Print PythonNormsInv2.PyNormStdInv(0.95)
End Sub

Sub ResetCoin()
    vExcelCoinPrice = 1.2
End Sub

Sub TestOnTime()
    
    '*
    '* initialize price of ExcelCoin
    '*
    If IsEmpty(vExcelCoinPrice) Then
        vExcelCoinPrice = 1.2
    End If

    Const dDrift As Double = 1.0001
    
    Dim dRectangular As Double
    dRectangular = Rnd(1)
    
    Dim dNormal2 As Double
    If WinWordVBA() Then
        dNormal2 = PythonNormsInv2.PyNormInv(dRectangular, 0, 0.001)
    ElseIf ExcelVBA() Then
        '=NORM.INV(0.95,0,0.001)
        Dim objApp As Object
        Set objApp = Application
        dNormal2 = objApp.WorksheetFunction.NormInv(dRectangular, 0, 0.001)
    
    End If
    
    Dim dLogNormal As Double
    dLogNormal = Exp(dNormal2)
    
    vExcelCoinPrice = vExcelCoinPrice * dLogNormal * dDrift
    Debug.Print vExcelCoinPrice

    '*
    '* call Redis to update price of Excel Coin, this is just a number (double)
    '*
    Dim oRedisSocket As Object
    Set oRedisSocket = RedisSocket
    Dim sResponse As String
    sResponse = oRedisSocket.SendAndReadReponse("SET USD/ExcelCoin " & CStr(vExcelCoinPrice) & vbCrLf)
    Dim vParsed As Variant
    vParsed = oRedisSocket.Parse(sResponse)
    
    '*
    '* also write the fuller meta document that carries timestamps, source URI etc.
    '*
    Dim sJsonDoc As String
    sJsonDoc = "{ ""timestamp2"": " & CDbl(Now()) & ", ""redisId"": ""USD/ExcelCoin/meta"" ,  ""point"": " & vExcelCoinPrice & " }"
    sJsonDoc = VBA.Replace(sJsonDoc, """", "\""")
    sResponse = oRedisSocket.SendAndReadReponse("SET USD/ExcelCoin/meta """ & sJsonDoc & """" & vbCrLf)
    vParsed = oRedisSocket.Parse(sResponse)
    
    '*
    '* set up next call
    '*
    DoEvents
    Call Application.OnTime(Format(Now() + CDate("00:00:02"), "dd/mmm/yyyy hh:mm:ss"), "TestOnTime")
End Sub

Function ExcelVBA() As Boolean
    On Error GoTo QuickExit
    Dim objApp As Object
    Set objApp = Application
    
    Dim objWbs As Object
    Set objWbs = objApp.Workbooks
    
    ExcelVBA = True
QuickExit:
End Function

Function WinWordVBA() As Boolean
    On Error GoTo QuickExit
    Dim objApp As Object
    Set objApp = Application
    
    Dim objActiveDocument As Object
    Set objActiveDocument = objApp.ActiveDocument
    
    WinWordVBA = True
QuickExit:
End Function

About the C# RTD Server Code

The listing is quite large and worth talking through. The project type is a C# assembly with the 'Register for COM interop' checkbox checked (found on the Build tab of the project properties). Registering COM components requires administrator rights so run Visual Studio with admin rights. In AssemblyInfo.cs I have set [assembly: ComVisible(true)]

The RTD interfaces are defined in the Excel type library. So one must add a reference to Microsoft.Office.Interop.Excel primary interop assembly (PIA).

Also, I do some JSON processing so I have added the Newtonsoft.Json package by using the Package Manager.

Code Reuse

I do try to write object orientated code. In the listing below, there are three major classes plus some minor helper classes. The code split is intended to promote code re-use. Firstly, there is the RTDServer class which houses the RTD interface implementation. Secondly, there is the RedisSocket class which houses code for interacting with Redis via Sockets. Hopefully those two will be quite reusable.

Thirdly, the RedisRTDServer class bridges the RTDServer and RedisSocket classes. This is less likely to be reusable as it is very much idiosyncratic to this application.

DebugView and Error Handling

One potential impediment to re-use is error handling. The RTD function will smother any errors, if I want to communicate an error I need to write it to a log or call the Win32 Api OutputDebugString function and view the message using the DebugView application from SysInternals. Also, I can pass back the error message so it is written to calling cell. In the code I do both.

To facilitate this I subclass the C# Exception class to give a DebugMonitorableException class.

Error handling can be quite a personal choice so feel free to take a different view on error handling to me.

COM Interfaces

Two of the three major classes, RedisSocket and RedisRTDServer also have COM interfaces which allows them to be callable from VBA. This promotes re-use. I like to develop classes in C# whilst writing unit tests in Excel VBA. This is frequently how my classes mature and evolve.

Singleton substitute for global variable

We all know that global variables are naughty and in fact impossible in C#. However, I needed a singleton instance of the RedisSocket class to be initialised and be available thereafter. I found that creating and disposing the class every time I wanted to read a price caused the RTD server code to freeze. Moving to a singleton instance solved this.

RTDServer implements IRTDServer

There is a standard interface IRTDServer that any RTD server class must implement. One can read the Microsoft official documentation here. Actually better to read this superb Learning Tree blog post here.

In fact that Learning Tree blog post is probably a better place to begin and once you have that running correctly you can return to this blog post to see how to implement multiple topics as served by Redis.

RedisSocket encapsulates Redis Sockets interface

The RedisSocket class encapsulates a minimalistic interface to Redis. There are more comprehensive C# Redis interface libraries if you want them but for this blog this code will serve. The code uses the .NET TcpClient class to send and receive bytes via Sockets to/from Redis. The code parses the response using a simple split function. It is based on some equally simple VBA code I wrote in previous blog posts. Nevertheless it serves. I won't comment further on it here. See the previous two Redis blog posts for more details.

RedisRTDServer houses application specific code

As mentioned above, in order to make the RedisSockets and RTDServer classes as re-usable as possible I felt it necessary to offload as much as possible into a third class, RedisRTDServer, which houses all the code idiosyncratic to this application. It is called by the RTDServer class and is handed an array of strings as parameters.

The RedisRTDServer class interprets this array of strings. This is the role of the Router() method. The first string is the key; for the ExcelCoin the key is 'USD/ExcelCoin'. The second string specifies a handler which allows different processing logic, so if I want a single price then I supply "scalar" as the handler or indeed just an empty string ("").

I have added one other handler option, "json" and in this use case the key identifies a JSON document the third string is interpreted as a JSON path which allows a drill down into the JSON document.

Conceivably, in the future I could add another option "xml" and allow an XPath expression to be supplied to drill down into an Xml document.

I think you'll agree, there is plenty of scope to define a detailed interface.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Timers;
using System.Text;
using System.Reflection;

// Added Reference to Microsoft Excel 15
using Microsoft.Office.Interop.Excel;
using Newtonsoft.Json.Linq;  // I used Package Manager to add Newtonsoft.Json

namespace RedisRTDServerLib
{
    [ComVisible(false)]
    static class Win32Api
    {   // this is so I can use DebugView from SysInternals
        [DllImport("kernel32.dll")]
        public static extern void OutputDebugString(string lpOutputString);
    }

    [ComVisible(false)]
    static class Singletons
    {   // this Singleton class is in lieu of a global variable
        static RedisSocket _redisSocket = null;

        static public RedisSocket RedisSocketSingleton
        {
            get
            {
                if (_redisSocket == null)
                {
                    _redisSocket = new RedisSocket();
                    _redisSocket.Initialize();
                }
                return _redisSocket;
            }
            set
            {
                // only interested in this for tidy up purposes
                if (value == null)
                {
                    _redisSocket = value;
                }
            }
        }
    }

    [ComVisible(false)]
    public class DebugMonitorableException : Exception
    {   // this class does the same as an Exception but simply prints the error message to the debug monitor
        public DebugMonitorableException()
        {
        }

        public DebugMonitorableException(string message) : base(message)
        {
            Win32Api.OutputDebugString(message);
        }

        public DebugMonitorableException(string message, Exception inner) : base(message, inner)
        {
            Win32Api.OutputDebugString(message);
        }
    }

    [ComVisible(true)]
    public interface IRedisSocket
    {
        void Initialize();
        void Dispose();
        string SendAndReadReponse(string sCommand);
        object Parse(string sRaw);
    }
    [ClassInterface(ClassInterfaceType.None), ComVisible(true)]
    [ComDefaultInterface(typeof(IRedisSocket))]
    public class RedisSocket : IRedisSocket, IDisposable
    {
        TcpClient _client;

        public void Initialize()
        {
            try
            {
                _client = new TcpClient();
                _client.Connect("127.0.0.1", 6379);
            }
            catch (Exception ex)
            {
                throw new DebugMonitorableException("Trapped error: " + ex.Message);
            }
        }

        public void Dispose()
        {
            _client.Close();

            _client.Dispose();
            _client = null;
        }

        public string SendAndReadReponse(string sCommand)
        {
            try
            {
                //TcpClient client = new TcpClient();
                NetworkStream stream;

                stream = _client.GetStream();

                Byte[] sendBytes = Encoding.UTF8.GetBytes(sCommand);
                stream.Write(sendBytes, 0, sendBytes.Length);
                stream.Flush();

                Byte[] recvBytes = new byte[_client.ReceiveBufferSize];
                stream.Read(recvBytes, 0, recvBytes.Length);

                string result = Encoding.UTF8.GetString(recvBytes);


                string result2 = result.Substring(0, result.LastIndexOf("\r\n"));
                return result2;
            }
            catch (Exception ex)
            {
                throw new DebugMonitorableException("SendAndReadReponse error: " + ex.Message);
            }
        }

        public object Parse(string sResponse)
        {
            try
            {
                Int32 lTotalLength = sResponse.Length;
                Debug.Assert(lTotalLength > 0);

                string[] vSplitResponse;
                {
                    string[] splitStrings = { "\r\n" };
                    vSplitResponse = sResponse.Split(splitStrings, StringSplitOptions.None);
                }

                Int32 lReponseLineCount = vSplitResponse.Length;

                switch (sResponse.Substring(0, 1))
                {
                    case "$":
                        if (sResponse == "$-1")
                        {
                            throw new Exception("Not found!");
                        }
                        else
                        {
                            return vSplitResponse[1];
                        }
                    case "+":
                        return vSplitResponse[0].Substring(1);
                    case ":":

                        //'* response is a numeric
                        return Double.Parse(vSplitResponse[0].Substring(1));
                    case "-":
                        //'* response is an error
                        throw new Exception(vSplitResponse[0].Substring(1));
                    case "*":
                        //'* multiple responses, build an array to return
                        Int32 lResponseCount = Int32.Parse(vSplitResponse[0].Substring(1));
                        if (lResponseCount > 0)
                        {
                            Debug.Assert(lResponseCount == (lReponseLineCount - 1) / 2);
                            List<Object> returnList = new List<Object>();
                            for (Int32 lLoop = 0; lLoop < lResponseCount; lLoop++)
                            {
                                returnList.Add(vSplitResponse[(lLoop + 1) * 2]);
                            }
                            return returnList.ToArray();
                        }
                        else
                        {
                            return false;
                        }
                    default:
                        // '* this should not happen
                        throw new Exception("Unrecognised return type '" + sResponse.Substring(0, 1) + "'");
                }
            }
            catch (Exception ex)
            {
                throw new DebugMonitorableException("Parse error: " + ex.Message);
            }
        }
    }

    [ComVisible(true)]
    public interface IRedisRTDServer
    {
        object Router(Array Strings);
        string GetJsonDocOrError(Array Strings);
        Boolean GetJSON(string sKey, string path, out string json, out string errorMsg);
        string GetKey(Array Strings);
        string GetPath(Array Strings);
        string GetHandler(Array Strings);
        object GetScalarOrError(Array Strings);
        Boolean GetScalar(string sKey, out double scalar, out string errorMsg);
    }

    [ClassInterface(ClassInterfaceType.None), ComVisible(true)]
    [ComDefaultInterface(typeof(IRedisRTDServer))]
    public class RedisRTDServer : IRedisRTDServer
    {
        public object Router(Array Strings)
        {
            try
            {
                string handler = GetHandler(Strings);
                string path = GetPath(Strings);

                switch (handler)
                {
                    case "":
                    case "scalar":
                        return GetScalarOrError(Strings);
                        
                    case "json":
                        return GetJsonDocOrError(Strings);
                        
                    default:
                        return "Failed to request with handler '" + handler + "'";
                }
            }
            catch (Exception ex)
            {
                string errMsg = this.GetType().Name + '.' + MethodBase.GetCurrentMethod() + " error:" + ex.Message;
                Win32Api.OutputDebugString(errMsg);
                return errMsg;
            }
        }

        public string GetJsonDocOrError(Array Strings)
        {
            try
            {
                string sKey = GetKey(Strings);
                string path = GetPath(Strings);
                if (GetJSON(sKey, path, out string json, out string errMsg))
                {
                    return json;
                }
                else
                {
                    return errMsg;
                }
            }
            catch (Exception ex)
            {
                string errMsg = this.GetType().Name + '.' + MethodBase.GetCurrentMethod() + " error:" + ex.Message;
                Win32Api.OutputDebugString(errMsg);
                return errMsg;
            }
        }

        public Boolean GetJSON(string sKey, string path, out string json, out string errorMsg)
        {

            json = "";
            errorMsg = "";
            try
            {

                object parsed = null;

                try
                {
                    RedisSocket redisSocket = null;
                    redisSocket = Singletons.RedisSocketSingleton;
                    string rawResponse = redisSocket.SendAndReadReponse("GET " + sKey + "\r\n");
                    parsed = redisSocket.Parse(rawResponse);
                }
                catch (Exception ex)
                {
                    // for the time being I'm not rethrowing this
                    errorMsg = ex.Message;
                    return false;
                }
                if (parsed is String)
                {
                    json = (string)parsed;

                    if (path.Length == 0)
                    {
                        return true;
                    }
                    else
                    {   // NewtonSoft.JSON code to drill into a JSON document with a JSON path
                        JObject o = JObject.Parse(json);
                        JToken acme = o.SelectToken(path);
                        json = acme.ToString();
                        return true;
                    }
                }
                else
                {
                    errorMsg = "Not a JSON document!";
                    return false;
                }

            }
            catch (Exception ex)
            {
                throw new DebugMonitorableException(this.GetType().Name + '.' + MethodBase.GetCurrentMethod() + ": error\r\n" + ex.Message);
            }
        }

        public string GetKey(Array Strings)
        {
            try
            {
                string sKey;
                sKey = (string)Strings.GetValue(0);
                if (sKey.Contains(" "))
                {
                    sKey = "'" + sKey + "'";
                }
                return sKey;
            }
            catch (Exception ex)
            {
                string errMsg = this.GetType().Name + '.' + MethodBase.GetCurrentMethod() + " error:" + ex.Message;
                Win32Api.OutputDebugString(errMsg);
                throw new Exception(errMsg);
            }
        }

        public string GetPath(Array Strings)
        {
            try
            {
                string path = "";
                {
                    if (Strings.Length >= 3)
                    {
                        path = (string)Strings.GetValue(2);
                    }
                }
                return path;
            }
            catch (Exception ex)
            {
                string errMsg = this.GetType().Name + '.' + MethodBase.GetCurrentMethod() + " error:" + ex.Message;
                Win32Api.OutputDebugString(errMsg);
                throw new Exception(errMsg);
            }
        }

        public string GetHandler(Array Strings)
        {
            try
            {
                string handler = "";
                {
                    if (Strings.Length >= 2)
                    {
                        handler = (string)Strings.GetValue(1);
                    }
                }
                return handler;
            }
            catch (Exception ex)
            {
                string errMsg = this.GetType().Name + '.' + MethodBase.GetCurrentMethod() + " error:" + ex.Message;
                Win32Api.OutputDebugString(errMsg);
                throw new Exception(errMsg);
            }
        }

        public object GetScalarOrError(Array Strings)
        {
            try
            {
                string sKey = GetKey(Strings);

                if (GetScalar(sKey, out double price, out string errMsg))
                {
                    return price;
                }
                else
                {
                    return errMsg;
                }
            }
            catch (Exception ex)
            {
                string errMsg = this.GetType().Name + '.' + MethodBase.GetCurrentMethod() + " error:" + ex.Message;
                Win32Api.OutputDebugString(errMsg);
                return errMsg;
            }
        }

        public Boolean GetScalar(string sKey, out double scalar, out string errorMsg)
        {

            scalar = 0;
            errorMsg = "";
            try
            {

                object parsed = null;

                try
                {
                    RedisSocket redisSocket = null;
                    
                    redisSocket = Singletons.RedisSocketSingleton;
                    string rawResponse = redisSocket.SendAndReadReponse("GET " + sKey + "\r\n");
                    parsed = redisSocket.Parse(rawResponse);
                }
                catch (Exception ex)
                {
                    // for the time being I'm not rethrowing this
                    errorMsg = ex.Message;
                    return false;
                }
                if (parsed is String)
                {
                    string parsedString = (string)parsed;
                    if (!Double.TryParse(parsedString, out scalar))
                    {   
                        throw new DebugMonitorableException(this.GetType().Name + '.' + MethodBase.GetCurrentMethod() + ": could not parse '" + parsedString + "' to double");
                    }
                }

                return true;
            }
            catch (Exception ex)
            {
                throw new DebugMonitorableException(this.GetType().Name + '.' + MethodBase.GetCurrentMethod() + ": error\r\n" + ex.Message);
            }
        }
    }


    public class RTDServer : IRtdServer
    {
        private Dictionary<int, Array> _topicsAndStrings = new Dictionary<int, Array>();
        private IRTDUpdateEvent m_callback;
        private Timer m_timer;
        private int m_topicId;

        int IRtdServer.ServerStart(IRTDUpdateEvent CallbackObject)
        {
            try
            {
                m_callback = CallbackObject;
                m_timer = new Timer();
                m_timer.Elapsed += new ElapsedEventHandler(TimerEventHandler);
                m_timer.Interval = 3000; // in milliseconds
                return 1;
            }
            catch (Exception ex)
            {
                Win32Api.OutputDebugString("IRtdServer.ServerStart:" + ex.Message);
                return 0;
            }
        }

        dynamic IRtdServer.ConnectData(int TopicID, ref Array Strings, ref bool GetNewValues)
        {
            try
            {
                _topicsAndStrings.Add(TopicID, Strings);

                m_topicId = TopicID;
                m_timer.Start();
                return GetTime();
            }
            catch (Exception ex)
            {
                string errMsg = "IRtdServer.ConnectData error:" + ex.Message;
                Win32Api.OutputDebugString(errMsg);
                return errMsg;
            }
        }

        private string GetTime()
        {
            return "RTD Server GetTime method: " + DateTime.Now.ToString("hh: mm:ss");
        }

        private void TimerEventHandler(object sender, EventArgs args)
        {
            try
            {
                // UpdateNotify is called to inform Excel that new data are available
                // the timer is turned off so that if Excel is busy, the TimerEventHandler is not called repeatedly

                m_timer.Stop();
                m_callback.UpdateNotify();
            }
            catch (Exception ex)
            {
                Win32Api.OutputDebugString("TimerEventHandler:" + ex.Message);
            }
        }

        Array IRtdServer.RefreshData(ref int TopicCount)
        {
            object[,] data = null;
            try
            {
                TopicCount = _topicsAndStrings.Count;
                data = new object[2, TopicCount];

                RedisRTDServer redisRtdServer = new RedisRTDServer();
                int idx = 0;
                foreach (var kvp in _topicsAndStrings)
                {
                    Array Strings = kvp.Value;
                    data[0, idx] = kvp.Key;
                    data[1, idx] = redisRtdServer.Router(kvp.Value);
                    idx++;
                }

                m_timer.Start();
                return data;
            }
            catch (Exception ex)
            {
                Win32Api.OutputDebugString("IRtdServer.RefreshData:" + ex.Message);
                return data;
            }
        }

        void IRtdServer.DisconnectData(int TopicID)
        {
            try
            {
                if (_topicsAndStrings.ContainsKey(TopicID))
                {
                    _topicsAndStrings.Remove(TopicID);
                }

                if (_topicsAndStrings.Count == 0)
                {
                    m_timer.Stop();
                    Singletons.RedisSocketSingleton = null;
                }
            }
            catch (Exception ex)
            {
                Win32Api.OutputDebugString("IRtdServer.DisconnectData:" + ex.Message);
            }
        }

        int IRtdServer.Heartbeat()
        {
            return 1;
        }

        void IRtdServer.ServerTerminate()
        {
            try
            {
                if (null != m_timer)
                {
                    m_timer.Dispose();
                    m_timer = null;
                }
                if (null != Singletons.RedisSocketSingleton)
                {
                    Singletons.RedisSocketSingleton = null;
                }
            }
            catch (Exception ex)
            {
                Win32Api.OutputDebugString("IRtdServer.ServerTerminate:" + ex.Message);
            }
        }
    }
}

Examples of calling from the worksheet

So finally, to call from the worksheet you need some cell formulas

=RTD("redisrtdserverlib.rtdserver",,"USD/ExcelCoin","","")
=RTD("redisrtdserverlib.rtdserver",,"USD/ExcelCoin/meta","json","$.timestamp2")+0