Mixing Legacy C++ with React

David Jaffe
The Startup
Published in
9 min readDec 14, 2019
Photo by Glenn Carstens-Peters on Unsplash

As an engineer primarily developing in legacy C++ with the Microsoft Foundation Class library (MFC) for the UI, creating attractive user-friendly interfaces can be extremely difficult, if not impossible in some cases. The time and effort required for something visually simple such as changing an input element’s text color or handling sizing logic is irritating. A nice slide out pane or aesthetically pleasing tooltips is out of the question to meet deadlines (and to keep from flipping a table in frustration). And don’t even consider PNG or SVG images — bitmaps and handling transparency by hand is your go-to.

Adding in the .NET framework is an option we experimented with, but only provides a slight improvement. Controls are a little easier to manipulate and provide a small step in the right direction in terms of aesthetics. However, a few extra tweaks are needed in Visual Studio’s settings to debug both native and managed C++ at the same time, and a C++/CLI wrapper is needed for communication between the two. If no other .NET technologies are being used in the stack, it can feel out of place.

Meanwhile, a large chunk of a React app could be prototyped by the time an OnCreate and OnSize could be finished in C++. The available components in C# are still no where near what can be done in modern HTML and JavaScript in terms of aesthetics. Some of the .NET components are just too rigid (i.e. TreeView) to tweak exactly how you want them to meet UI requirements. So why not just embed a React app inside C++?

If the React component is a simple, stand-alone piece, this is fairly easy. Embed an HTML View (MFC has a CHtmlView window), navigate to where the app lives, and that’s it. If data is needed to be passed, tacking it on as parameters to the URL is a quick and dirty solution. Rarely, however, would a UI element not need to handle user input and do something with that feedback, or receive extra information later on. This is where some creativity is needed to provide a communication channel between these two much different languages.

Part 1: Embedding a Browser

The first step is setting up an embedded Internet Explorer control that can load the web app. We’ll be using MFC’s CHtmlView, and starting by creating a new skeleton class which implements this view:

MyHtmlView.h

#pragma once
#include <afxhtml.h>
class CMyHtmlView : public CHtmlView
{
DECLARE_DYNCREATE(CMyHtmlView)

public:
CMyHtmlView() = default;
virtual ~CMyHtmlView() = default;
BOOL CreateControl(DWORD dwStyle, const CRect& rect, CWnd* pParentWnd, UINT nID);

protected:
DECLARE_MESSAGE_MAP()
virtual void PostNcDestroy();
};

MyHtmlView.cpp

#include "pch.h"
#include "MyHtmlView.h"
IMPLEMENT_DYNCREATE(CMyHtmlView, CHtmlView)
BEGIN_MESSAGE_MAP(CMyHtmlView, CHtmlView)

END_MESSAGE_MAP()
BOOL CMyHtmlView::CreateHTMLControl(DWORD dwStyle, const CRect& rect, CWnd* pParentWnd, UINT nID)
{
BOOL bStatus = Create(NULL, NULL, dwStyle, rect, pParentWnd, nID, NULL);
if (bStatus)
{
Navigate(_T("http://www.google.com"));
}
return bStatus;
}
void CMyHtmlView::PostNcDestroy()
{
}

We’re just going to use this inside a simple modal dialog application, so we’ll add an instance of this to our dialog and create it:

// header
class CMFCApplicationDlg : public CDialogEx
{
...
private:
CMyHtmlView m_htmlView;
...
}
// cpp
...
int CMFCApplicationDlg::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
...
m_htmlCtrl.CreateHTMLControl(WS_CHILD | WS_VISIBLE, CRect(0, 0, 0, 0), this, 110);
...
}
void CMFCApplicationDlg::OnSize(UINT nType, int cx, int cy)
{
CDialogEx::OnSize(nType, cx, cy);
m_htmlCtrl.MoveWindow(0, 0, cx, cy);
}

Compile and run, and a dialog opens with an embedded IE control.

Nothing really out of the ordinary, so let’s set this up for calling into JavaScript.

Part 2: Invoking JavaScript from C++

Calling code directly from one language to another requires some sort of interface, and since we’re starting in C++, this will rely on COM. At a high level, we’re going to find a JavaScript function by its name inside the web page loaded in the IE control, and directly invoke it.

CString CMyHtmlView::InvokeJavaScript(LPCTSTR pszName, UINT nNumberOfParameters, ...)
{
CString sReturnValue;
LPDISPATCH pDocumentDispatch = GetHtmlDocument();
if (pDocumentDispatch == NULL)
{
return sReturnValue;
}
IHTMLDocument2* pDocument = NULL;
pDocumentDispatch->QueryInterface(IID_IHTMLDocument2, (void**)&pDocument);
pDocumentDispatch->Release();
pDocumentDispatch = NULL;
if (pDocument == NULL)
{
return sReturnValue;
}
IDispatch* pScripts = NULL;
pDocument->get_Script(&pScripts);
if (pScripts == NULL)
{
return sReturnValue;
}
LPOLESTR oleName = T2OLE((LPTSTR)pszName);
DISPID dispatchId = 0x00;
if (FAILED(pScripts->GetIDsOfNames(IID_NULL, &oleName, 1, LOCALE_SYSTEM_DEFAULT, &dispatchId)))
{
return sReturnValue;
}
BYTE parameterTypes[] = VTS_BSTR VTS_BSTR VTS_BSTR VTS_BSTR VTS_BSTR;
parameterTypes[nNumberOfParameters] = 0;
COleVariant vReturnValue;

TRY
{
COleDispatchDriver dispatchDriver(pScripts);
va_list arguments;
va_start(arguments, nNumberOfParameters);
dispatchDriver.InvokeHelperV(dispatchId, DISPATCH_METHOD, VT_VARIANT, (void*)&vReturnValue, parameterTypes, arguments);
va_end(arguments);
if (vReturnValue.vt == VT_BSTR)
{
sReturnValue = CString(vReturnValue.bstrVal);
}
}
CATCH(CException, error)
{
if (error != NULL)
{
error->ReportError();
}
}
END_CATCH
return sReturnValue;
}

Let’s go through this block by block to see what occurs at a lower level.

Get a pointer to the IHTMLDocument2 interface

CString sReturnValue;
LPDISPATCH pDocumentDispatch = GetHtmlDocument();
if (pDocumentDispatch == NULL)
{
return sReturnValue;
}
IHTMLDocument2* pDocument = NULL;
pDocumentDispatch->QueryInterface(IID_IHTMLDocument2, (void**)&pDocument);
pDocumentDispatch->Release();
pDocumentDispatch = NULL;
if (pDocument == NULL)
{
return sReturnValue;
}

This is using COM’s IUnknown QueryInterface method to get a pointer to the IHTMLDocument2 interface, with some NULL checks for safety. Nothing too fancy yet.

Find a JavaScript function

IDispatch* pScripts = NULL;
pDocument->get_Script(&pScripts);
if (pScripts == NULL)
{
return sReturnValue;
}
LPOLESTR oleName = T2OLE((LPTSTR)pszName);
DISPID dispatchId = 0x00;
if (FAILED(pScripts->GetIDsOfNames(IID_NULL, &oleName, 1, LOCALE_SYSTEM_DEFAULT, &dispatchId)))
{
return sReturnValue;
}

Here we’re using get_Script on the IHTMLDocument2 interface to get an IDispatch pointer, casting our function name pszName into another form, and using the GetIDsOfNames found on IDispatch. This will attempt to find a function by the given name in the script objects living in the IE window, and set the passed in dispatch ID reference if found.

Invoking the function

BYTE parameterTypes[] = VTS_BSTR VTS_BSTR VTS_BSTR VTS_BSTR VTS_BSTR;
parameterTypes[nNumberOfParameters] = 0;
COleVariant vReturnValue;

TRY
{
COleDispatchDriver dispatchDriver(pScripts);
va_list arguments;
va_start(arguments, nNumberOfParameters);
dispatchDriver.InvokeHelperV(dispatchId, DISPATCH_METHOD, VT_VARIANT, (void*)&vReturnValue, parameterTypes, arguments);
va_end(arguments);
if (vReturnValue.vt == VT_BSTR)
{
sReturnValue = CString(vReturnValue.bstrVal);
}
}
CATCH(CException, error)
{
if (error != NULL)
{
error->ReportError();
}
}
END_CATCH
return sReturnValue;

Since we have an ID of the function, we can directly invoke it using a COleDispatchDriver. We start by setting up an array of BSTR types so that OLE knows we’re sending strings through COM. We create a list out of the passed in strings, and use InvokeHelperV to invoke our JavaScript function with the passed in strings. I personally only use one parameter — a single JSON payload that can be converted into an object in the JavaScript side to be used as a React prop.

In theory, all this could be unnecessary if data is known beforehand and passed in via the URL. However this method allows delaying rendering until the data is known at a specific point in a workflow.

Part 3: The React App

We’ll start with a simple empty React app via npx create-react-app simple-app. Ensure it runs with npm start, turn on relative paths for build time (I use the homepage setting in package.json), then let’s tweak the entry point to allow COM to invoke it. Open up the generated index.js, and wrap the render in an event listener:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
document.addEventListener('InitializeFromPayload', function (event) {
ReactDOM.render(<App payload={event.payload} />,
document.getElementById('root'));
});
serviceWorker.unregister();

This allows us to delay anything from occurring in the React side until we say so. Another option is leaving the auto-generated render as is, and have the app use an empty prop as a flag to show a loading screen. Then, add the above event-wrapped render as the payload receiver to pass in the payload as a prop and re-render the app. Note that we have a payload prop set as the event’s payload.

The next change needed in our React app is in the index.html. First, we want to expose a function that COM will be looking for in a script object. Add the following to the end of the <body> tag:

<script type="text/javascript">
function InitializeFromPayload(payload) {
var event = document.createEvent('event');
event.initEvent("InitializeFromPayload", true, true);
event.payload = payload;
document.dispatchEvent(event);
}
</script>

This is what get_Scripts back in C++ will receive, which we then look through for a function matching a provided name via GetIDsOfNames.

The last change, still in index.html, is setting the IE mode. When embedding an Internet Explorer window, the lowest possible version of IE is used by default (generally IE6) for that process. Obviously that isn’t going to work for React without a ton of polyfills, so we’ll just force the page to render in something higher. Adding this to the <head> tag will take care of that.

<meta http-equiv="X-UA-Compatible" content="IE=11" />

Part 4: Initializing the React App

Now to wrap up the C++ to JavaScript flow. Somewhere in our dialog, we’ll call our InvokeJavaScript method in our HTML view with a payload.

m_htmlCtrl.InvokeJavaScript(_T("InitializeFromPayload"), 1, _T("Hello from MFC"));

Here is where we pass in the JavaScript function that we defined in index.html, and one string that gets passed into it. I tweaked my React app to use that prop as a simple display, changed my CMyHtmlView::CreateHTMLControl to navigate to where my React app’s index.html is, and success! When the above InvokeJavaScript method is called, we’ll have our React app displaying in the embedded IE control with our passed in payload from C++.

Part 5: Communication Back to C++

We’ve gotten one direction of communication established, so now it’s time to go the other way — JavaScript to the owning C++ process. We don’t have Node running to hit C++ libraries, IE isn’t going to let you safely do whatever you want from JavaScript, and we want to communicate back to the already running application — not just hitting a library. In order to achieve this, we’re going to make use of CHtmlView::OnBeforeNavigate2.

This function is called right before any navigation event happens in the IE browser, and allows us to override what happens with that navigation — including stopping it altogether. We can “navigate” to a specific payload instead of a URL from our React app, and intercept that navigation if this is a payload rather than a true URL.

void CMyHtmlView::OnBeforeNavigate2(LPCTSTR lpszURL, DWORD nFlags, LPCTSTR lpszTargetFrameName, CByteArray& baPostedData, LPCTSTR lpszHeaders, BOOL* pbCancel)
{
CWnd* pParent = GetParent();
if (pParent != NULL)
{
if (_tcsncicmp(lpszURL, _T("mymsg"), 5) == 0)
{
*pbCancel = TRUE;
// Do something with lpszURL
::MessageBox(this->GetSafeHwnd(), lpszURL, lpszURL, 0);
}
}
}

With the above, we check if the URL attempting to be navigated to starts with “mymsg”. If it does, we cancel the navigation, and that “URL” can be treated as a payload to perform logic in our C++ application. If it doesn’t, the navigation continues on as expected. Let’s add a link in our React app that will be handled by our C++:

<a
className="App-link"
href="mymsg:mypayload"
>
Talk to C++
</a>

When clicking that link, we will now see a message box from our C++ application with our payload.

This mechanism opens up creating seamless communication between legacy front-end code and JavaScript web apps. To an end-user, they’re not aware of any behind the scenes interacting between two entirely different technologies; they just see a polished, modern UI that everyone now expects. To a front-end engineer working in desktop applications, they aren’t pigeonholed to only working in legacy code, or stuck not being able to create something as flashy as what their web-based counterparts are developing. To an architect or project manager, weeks of engineering time generally spent on all the usual boilerplate for C++ visuals can now be used for extra features that would usually be scoped out.

I hope this allows you to enhance your existing legacy Visual C++ code base with more modern UI development, or to come up with other creative ways to use two very different technologies in the same layer of a stack.

--

--