How to run regasm.exe from a C++ program (.NET 4) - c++

I need to register a .NET COM dll from a C++ program that is using it. For .NET versions older then .NET 4 this is explained in How to run regasm.exe from a C++ program?. Following is the minimal code (no checks) that provides the path to an older version of the CLR.
CComBSTR mscoreeName("mscoree.dll");
HINSTANCE hMscoree = CoLoadLibrary(mscoreeName, FALSE);
typedef HRESULT (WINAPI *LPFNGETCORSYSDIR)(LPWSTR, DWORD, DWORD*);
LPFNGETCORSYSDIR lpfunc = (LPFNGETCORSYSDIR)GetProcAddress(hMscoree,_T("GetCORSystemDirectory"));
DWORD bufferSize = 256;
DWORD bufferUsed;
LPWSTR pwzBuffer = new WCHAR[bufferSize];
(*lpfunc)(pwzBuffer, bufferSize, &bufferUsed);
However since I use .NET 4 the method GetCORSystemDirectory is superseded by the ICLRRuntimeInfo::GetRuntimeDirectory which is not an entry point in mscoree.dll (checked with depends). According the documentation on MSDN the method is included as a resource in MSCorEE.dll.
Question is how to get access to this method from C++?
Besides that I'm wondering if there is no easier way...

The problem with the way of working in the question is in finding the correct location of RegAsm. Thanks to the comment of Hans Passant to use RegistrationService.RegisterAssembly I changed the ClassLibrary into a self-registering executable.
static void Main(string[] args)
{
if (args.Length != 1)
{
ShowHelpMessage();
return;
}
if (args[0].CompareTo("/register") == 0)
{
Assembly currAssembly = Assembly.GetExecutingAssembly();
var rs = new RegistrationServices();
if (rs.RegisterAssembly(currAssembly, AssemblyRegistrationFlags.SetCodeBase))
{
Console.WriteLine("Succesfully registered " + currAssembly.GetName());
} else
{
Console.WriteLine("Failed to register " + currAssembly.GetName());
}
return;
}
if (args[0].CompareTo("/remove") == 0)
{
Assembly currAssembly = Assembly.GetExecutingAssembly();
var rs = new RegistrationServices();
if (rs.UnregisterAssembly(currAssembly))
{
Console.WriteLine("Succesfully removed " + currAssembly.GetName());
}
else
{
Console.WriteLine("Failed to remove " + currAssembly.GetName());
}
return;
}
ShowHelpMessage();
}

Related

C++ GetModuleFileName using Boost

I have the following code that needs to be ported from windows to boost:
BOOL Class::fn_GetModulePath(WCHAR szPath[MAX_PATH])
{
BOOL bReturn = FALSE;
dll::library_handle hDll = dll::load_shared_library((const char*)DC_DLL_FILENAME);
//HMODULE hDll = LoadLibrary(DC_DLL_FILENAME);
if (hDll)
{
// This function needs replacing
DWORD dwResult = GetModuleFileName(hDll,szPath,MAX_PATH);
dll::close_shared_library(hDll);
//FreeLibrary(hDll);
if (dwResult)
{
int iLen = (int) wcslen(szPath);
if (iLen)
{
for (int i = iLen; i >= 0; i--)
{
if(szPath[i] == '\\')
{
szPath[i+1] = 0;
break;
}
}
}
bReturn = TRUE;
}
}
return bReturn;
}
How would I go about implementing the GetModuleFileName function using Boost?
Any help appreciated!
boost::dll::shared_library class has a method location which returns the full path to the library.
For the whole program, there is boost::dll::program_location global function.
In addition, it is possible to find the executable or library location by symbol address and by source location:
boost::dll::symbol_location
boost::dll::this_line_location
The latter can only be used by a module to find its own location.
You can use Boost.Dll like so:
shared_library lib(DC_DLL_FILENAME);
filesystem::path full_path = lib.location();
If you're trying to get the path to the currently running code, that is boost::dll::this_line_location().

Dynamic loading Leadtools DLLs

I am using Leadtools 17.5. If I statically link the Leadtools Dlls into my 64 bit C++ Application and then call L_SetLicenseBuffer everything works fine and the return value is zero. But for security reasons, the final product is not allowed to add those DLLs into the System32 folder and is also not allowed to change the system path, and since multiple applications are using the tools I want to install them in a common folder (C:\Program Files\Common Files\LeadTools\17.5 for example) and use AddDllDirectory to add the path to the DLL search path. So I decided to load the DLLs dynamically at the run-time. So I created a definition for the function like this:
typedef L_INT (EXT_FUNCTION* TL_SetLicenseBuffer)(L_UCHAR* pLicenseBuffer, L_SSIZE_T nSize, L_TCHAR* pszDeveloperKey);
typedef L_BOOL (EXT_FUNCTION* TL_IsSupportLocked)(L_UINT uType);
then created a function pointer like this:
TL_SetLicenseBuffer pfSetLicenseBuffer = NULL;
TL_IsSupportLocked pfIsSupportLocked = NULL;
then add the paths to where the DLLs are to the DLL search path:
AddDllDirectory(LEAD_DLL_PATH);
AddDllDirectory(LEAD_FILTER_PATH);
and set the default directory search path for DLLs to be the user defined:
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_USER_DIRS);
then load the DLL and get the address of the functions I need:
HINSTANCE hKrn = LoadLibrary(L"ltkrnx.dll");
pfSetLicenseBuffer = (TL_SetLicenseBuffer)GetProcAddress(hKrn, "L_SetLicenseBuffer");
pfIsSupportLocked = (TL_IsSupportLocked)GetProcAddress(hKrn, "L_IsSupportLocked");
now if I use the function pointer with the same parameters as before, the function fails and returns -13 and any subsequent call to for example to pfIsSupportLocked shows the nag dialog:
retCode = pfSetLicenseBuffer(pLicenseData, LicSize, pKeyStr); // retCode is -13
pfIsSupportLocked(L_SUPPORT_DOCUMENT); // Shows nag dialog
Does anyone know how I can fix this?
Thank you
Sam
The first thing you need to do is check the debugger output and make sure that the DLL you are expecting to get loaded is the one getting loaded by verifying the path. It is possible that you have multiple versions of LTKRNX.DLL in your search path. I have tested your code here and it returned SUCCESS:
typedef L_INT (EXT_FUNCTION* TL_SetLicenseBuffer)(L_UCHAR* pLicenseBuffer, L_SSIZE_T nSize, L_TCHAR* pszDeveloperKey);
typedef L_BOOL (EXT_FUNCTION* TL_IsSupportLocked)(L_UINT uType);
HINSTANCE hKrn = LoadLibrary(L"ltkrnx.dll");
TL_SetLicenseBuffer pfSetLicenseBuffer = NULL;
TL_IsSupportLocked pfIsSupportLocked = NULL;
pfSetLicenseBuffer = (TL_SetLicenseBuffer)GetProcAddress(hKrn, "L_SetLicenseBuffer");
pfIsSupportLocked = (TL_IsSupportLocked)GetProcAddress(hKrn, "L_IsSupportLocked");
L_INT retCode = pfSetLicenseBuffer(szLICAnsi, _countof(szLICAnsi), pKeyStr);
if(retCode == SUCCESS)
bRet = pfIsSupportLocked(L_SUPPORT_DOCUMENT);
else
printf("Problem!");
Also what PaulMcKenzie suggested is another way to verify that your calls to LoadLibrary are working correctly. If you still cannot figure it out, you can contact our Technical Support to assist you with this issue at support#leadtools.com
I was not able to make the dynamic loading to work at all, but I was able to use Delay loading to work.What I had to do was to go back to linking the extracted .Lib files to my application and then tell compiler to load the associated DLLs with delay, which gave me a chance to create Notification Hooks to __pfnDliNotifyHook2 and __pfnDliFailureHook2 and that way I could use LoadLibrary to load the delayed loaded Dlls from correct location.But that only fixed half the problem because some of these Dlls are dependent on other DLLs and when I used the full path to load the DLL that I wanted, it could not find the secondary DLLs (which were located in the same directory as the one I was loading) and that would cause LoadLibrary to fail. The solution was to keep track of those dependencies and pre-load them. I am including some of the code to fix the issue for anyone whom might run into similar situation later on.P. S. I am using Embarcadero's C++ Builder, so Some of the objects like the Strings, TStringList and Exception may not be exactly what everyone is familiar with, but the concept should work in VC++ as well.
#include <map>
struct TDllDependency
{
TStringList* Dependency;
HMODULE hDll;
__fastcall TDllDependency(void)
{
hDll = NULL;
Dependency = new TStringList();
}
virtual __fastcall ~TDllDependency(void)
{
delete Dependency;
}
};
class TDllModList : public std::map<System::String, TDllDependency>
{
public:
void __fastcall CheckDependency(const System::String& aName);
};
//---------------------------------------------------------------------------
System::String __fastcall GetLtDllPath(void)
{
wchar_t* pfPath = NULL;
System::String dllPath;
SHGetKnownFolderPath(FOLDERID_ProgramFilesCommon, KF_FLAG_DEFAULT, NULL, &pfPath);
if (NULL != pfPath)
{
dllPath = IncludeTrailingBackslash(pfPath) + L"LeadTools\\17.5\\";
::CoTaskMemFree(pfPath);
}
return dllPath;
}
System::String mDllPath(GetLtDllPath());
TDllModList DllModList;
void __fastcall InitDllDepends()
{
DllModList.clear();
#if defined(_WIN64)
DllModList[L"ltimgefxx.dll"].Dependency->CommaText = L"ltdisx.dll,ltimgutlx.dll";
DllModList[L"ltefxx.dll"].Dependency->CommaText = L"ltdisx.dll,ltimgutlx.dll";
DllModList[L"ltimgcorx.dll"].Dependency->CommaText = L"ltdisx.dll,ltimgutlx.dll";
DllModList[L"ltdlgimgefxx.dll"].Dependency->CommaText = L"ltdisx.dll,ltdlgkrnx.dll,ltdlgcomx.dll,ltdlgctrlx.dll,ltdlgutlx.dll,ltimgefxx.dll,ltimgsfxx.dll,ltimgcorx.dll,ltimgclrx.dll";
DllModList[L"ltdlgutlx.dll"].Dependency->CommaText = L"ltdisx.dll,ltfilx.dll,ltdlgkrnx.dll,ltimgclrx.dll,ltimgcorx.dll,ltimgefxx.dll,ltimgsfxx.dll";
DllModList[L"ltdlgctrlx.dll"].Dependency->CommaText = L"ltdlgutlx.dll,ltdlgkrnx.dll,ltdisx.dll,ltfilx.dll,ltimgefxx.dll";
DllModList[L"ltdlgcomx.dll"].Dependency->CommaText = L"ltdlgkrnx.dll,ltdlgctrlx.dll,ltdlgutlx.dll";
#elif defined(__WIN32__)
DllModList[L"ltimgefxu.dll"].Dependency->CommaText = L"ltdisu.dll,ltimgutlu.dll";
DllModList[L"ltefxu.dll"].Dependency->CommaText = L"ltdisu.dll,ltimgutlu.dll";
DllModList[L"ltimgcoru.dll"].Dependency->CommaText = L"ltdisu.dll,ltimgutlu.dll";
DllModList[L"ltdlgimgefxu.dll"].Dependency->CommaText = L"ltdisu.dll,ltdlgkrnu.dll,ltdlgcomu.dll,ltdlgctrlu.dll,ltdlgutlu.dll,ltimgefxu.dll,ltimgsfxu.dll,ltimgcoru.dll,ltimgclru.dll";
DllModList[L"ltdlgutlu.dll"].Dependency->CommaText = L"ltdisu.dll,ltfilu.dll,ltdlgkrnu.dll,ltimgclru.dll,ltimgcoru.dll,ltimgefxu.dll,ltimgsfxu.dll";
DllModList[L"ltdlgctrlu.dll"].Dependency->CommaText = L"ltdlgutlu.dll,ltdlgkrnu.dll,ltdisu.dll,ltfilu.dll,ltimgefxu.dll";
DllModList[L"ltdlgcomu.dll"].Dependency->CommaText = L"ltdlgkrnu.dll,ltdlgctrlu.dll,ltdlgutlu.dll";
#endif
};
HMODULE SafeLoadLeadDll(const System::String tName)
{
System::String tPath;
HMODULE retVal = NULL;
DllModList.CheckDependency(tName);
tPath = mDllPath + tName;
if(FileExists(tPath))
retVal = ::LoadLibrary(tPath.c_str());
return retVal;
}
FARPROC WINAPI MyDliNotifyHook(unsigned dliNotify, PDelayLoadInfo pdli)
{
FARPROC retVal = NULL;
System::String tStr(pdli->szDll);
tStr = tStr.LowerCase();
if(dliNotePreLoadLibrary == dliNotify)
{
TDllModList::iterator i = DllModList.find(tStr);
if(DllModList.end() == i)
{
retVal = (FARPROC)SafeLoadLeadDll(tStr);
DllModList[tStr].hDll = (HMODULE)retVal;
}
else if(NULL == i->second.hDll)
{
i->second.hDll = SafeLoadLeadDll(tStr);
retVal = (FARPROC)i->second.hDll;
}
else
retVal = (FARPROC)i->second.hDll;
}
else if(dliFailLoadLib == dliNotify)
{
tStr = L"Compleatly falied to load " + tStr;
::OutputDebugString(tStr.c_str());
}
return retVal;
}
FARPROC WINAPI MyDliFailureHook(unsigned dliNotify, PDelayLoadInfo pdli)
{
FARPROC retVal = NULL;
if(dliNotePreLoadLibrary == dliNotify)
{
System::String tMsg = pdli->szDll;
tMsg = L"Failed to load \"" + tMsg + L"\".\n" + SysErrorMessage(::GetLastError());
throw Exception(tMsg);
}
return retVal;
}
extern "C" PfnDliHook __pfnDliNotifyHook2 = MyDliNotifyHook;
extern "C" PfnDliHook __pfnDliFailureHook2 = MyDliFailureHook;
void __fastcall TDllModList::CheckDependency(const System::String& aName)
{
TDllModList::iterator i = find(aName);
if(end() != i)
{
int len = i->second.Dependency->Count;
int j;
System::String tPath;
for(j = 0; j < len; j++)
{
if(end() == find(i->second.Dependency->Strings[j]))
{
CheckDependency(i->second.Dependency->Strings[j]);
tPath = mDllPath + i->second.Dependency->Strings[j];
(*this)[i->second.Dependency->Strings[j]].hDll = ::LoadLibrary(tPath.c_str());
}
}
}
}
//---------------------------------------------------------------------------
And of course InitDllDepends(); should be called at the beginning of WinMain to set things up correctly.

ADO called from C++ much slower in Windows Server 2008

We have some old C++ code which is being called from Classic ASP. It runs on Windows 2000 and is invoked using CreateObject via COM from the ASP. The code uses an ADO recordset (server side) which is passed to it from ASP and loops through the records, producing an XML file.
We have moved the C++ DLL onto a Windows 2008 server and it is a lot slower. Using Wireshark we can see this is because every time it moves to a a new record it restablishes the connection.
The old server used ADO 2.7 the new server uses ADO 6, but these are functionally equivalent and its just a new label for the version as I understand it.
Here is the main body of the code. It doesn't compile because I have removed bits to try and simplify the structure.
{
STDMETHODIMP Crender::generatexmldoc(LPDISPATCH adorecordset)
_Recordset rs;
rs.AttachDispatch(adorecordset);
CStringArray* fieldnames = new CStringArray();
int recno = 0;
LPDISPATCH fs = rs.GetFields();
while ( rs.GetState() == 1 && ! rs.GetEof() && recno++ != maxrecords) {
_generatexmlelement(fs , arecordname, TRUE, fieldnames);
rs.MoveNext();
}
fs->Release();
rs.DetachDispatch();
fieldnames->RemoveAll();
delete fieldnames;
}
void Crender::_generatexmlelement(LPDISPATCH adofields, LPCSTR elementname, BOOL closeelement, CStringArray* fieldnames)
{
USES_CONVERSION;
if (fieldnames->GetSize() == 0) {
for (index.uiVal = 0; index.uiVal < total; index.uiVal++) {
LPDISPATCH dItem = fs.GetItem(index);
fd.AttachDispatch(dItem);
CString name = fd.GetName();
fieldnames->Add(name);
fd.DetachDispatch();
dItem->Release();
}
}
writeoutfile(CString("<") + elementname);
for (index.uiVal = 0; index.uiVal < total; index.uiVal++) {
LPDISPATCH dItem = fs.GetItem(index);
fd.AttachDispatch(dItem);
value.Attach(fd.GetValue());
// code removed - data comes from value
writeoutfile(data, data.GetLength());
}
value.Clear();
value.Detach();
fd.DetachDispatch();
dItem->Release();
}
}
What in here could be causing the round trips to re-establish the connection?

Importing DLL's doesn't work the same (VS2003 to VS2010, multithreaded to multithreaded DLL)

During a massive code update from a mix of VC6, VS2003, and VS2005, I am running into a problem where VS2010 doesn't behave like VS2003 did. The application will scan a directory of DLL's and attempt to load them in one by one. This is done here:
CConfigPlugin::CConfigPlugin(LPCTSTR szPluginName)
{
ASSERT(szPluginName);
ASSERT(AfxIsValidString(szPluginName));
m_csFullpath = szPluginName;
m_hModule = LoadLibrary(m_csFullpath);
m_pInterface = (IConfigDllInterface *) NULL;
pInterface pPtr = pInterface(NULL);
if (m_hModule != NULL)
{
// If we loaded the DLL get the interface pointer
pPtr = pInterface(GetProcAddress(m_hModule, "GetInterface"));
}
if (pPtr != NULL)
{
pPtr(&m_pInterface);
}
else
{
::FreeLibrary(m_hModule);
m_hModule = HMODULE(NULL);
}
}
All DLL's show as being loaded:
...
'GenConfig.exe': Loaded 'C:\src\Debug\config\GenLogonConfig.dll', Symbols loaded.
'GenConfig.exe': Loaded 'C:\src\Debug\config\GenReportConfig.dll', Symbols loaded.
'GenConfig.exe': Loaded 'C:\src\Debug\config\ImportConfig.dll', Symbols loaded.
...
Each DLL has an identical GetInterface implementation shown below:
CConfigDllInterfaceImpl<CParentDlg> gdllObj;
BOOL GetInterface(IConfigDllInterface **ppPtr)
{
*ppPtr = &gdllObj;
// Temporary edit to test if gdllObj is set to proper parent.
CString name;
name = gdllObj.GetDisplayName();
// End edit
return true;
}
With a template as shown below:
__declspec(selectany) UINT guiAdvise;
template <class T> class CConfigDllInterfaceImpl : public IConfigDllInterface
{
public:
CConfigDllInterfaceImpl()
{
guiAdvise = RegisterWindowMessage(_T("GenConfig"));
m_pDlg = NULL;
}
virtual LPCTSTR GetDisplayName() const
{
static CString csTemp;
csTemp.LoadString(IDS_DISPLAY_NAME);
return csTemp;
}
// Can't be virtual because it uses the template T argument
BOOL DoModal(HWND hParent)
{
ASSERT(IsWindow(hParent));
if (m_pDlg == (T *) NULL)
{
m_pDlg = new T(CWnd::FromHandle(hParent));
return m_pDlg->Create();
}
else if (IsWindow(m_pDlg->GetSafeHwnd()))
{
m_pDlg->PostMessage(guiAdvise, eAdviseSwitchViews);
m_pDlg->SetActiveWindow();
}
return TRUE;
} // SNIP...
I can tell that my template isn't properly registering to its intended parent. GetDisplayName just returns "". My suspicion the cause of my problem is that I made a decision a month ago to change everything to Multithreaded DLL from Multithreaded. These are all MFC projects and it seemed the simplest and easiest way to just use _AFXDLL and make everything properly compile and link. All of the rest of my projects work fine, but I believe that because of the way this DLL is loaded:
CConfigDllInterfaceImpl gdllObj;
No longer works the way it used to.
So, question 1: Is my suspicion correct? Or am I completely offbase?
Question 2: If my suspicion is correct, how do I work around this? It isn't an option to go back to multithreaded at this point.
Thanks in advance.
I finally have some time to get back to answering this. Collin Dauphinee was correct in that it was a resource issue. I don't know why VS2003 is different from VS2010, but the solution was very simple.
CConfigPlugin::CConfigPlugin(LPCTSTR szPluginName)
{
ASSERT(szPluginName);
ASSERT(AfxIsValidString(szPluginName));
// Save off current Afx resource handle.
HINSTANCE hCurrentAfx = AfxGetResourceHandle(); // <--- Didn't need to
// do this before.
m_csFullpath = szPluginName;
m_hModule = NULL;
m_hModule = LoadLibrary(m_csFullpath);
m_pInterface = (IConfigDllInterface *) NULL;
pInterface pPtr = pInterface(NULL);
if (m_hModule != NULL)
{
AfxSetResourceHandle(m_hModule); // <--- here is where the resources
// get properly set. This is the
// solution to the problem.
// If we loaded the DLL get the interface pointer
pPtr = pInterface(GetProcAddress(m_hModule, "GetInterface"));
}
if (pPtr != NULL)
{
pPtr(&m_pInterface);
}
else
{
::FreeLibrary(m_hModule);
m_hModule = HMODULE(NULL);
}
// Now put Afx back.
AfxSetResourceHandle(hCurrentAfx);
}
I hope this helps someone else. I was stuck for days on this one.

How to Obtain the MSI 'UILevel' Property From a C++ Custom Action

I am trying to get at the 'UILevel' MSI property from within a C++ custom action in order to determine whether or not the user is running in 'no UI mode', but am not having much luck. The function I am calling is passed the MSIHANDLE from a function which I export in my DLL (which may be either a 'deferred' or 'firstsequence' action). What I'm seeing is that MsiGetPropertyW is always returning ERROR_MORE_DATA and the trueLength field is always 0. Here is my code:
bool runningInNoUIMode(MSIHANDLE hInstall)
{
unsigned long nBufLen = 64UL;
WCHAR *wszValue = new WCHAR[nBufLen];
DWORD trueLength = 0UL;
UINT result = ::MsiGetPropertyW(hInstall, L"UILevel", L"", &trueLength); // Get the size of the property value first to see if there is enough storage allocated.
if (ERROR_MORE_DATA == result || nBufLen <= trueLength)
{
if (NULL != wszValue)
{
delete [] wszValue;
}
// Allocate more memory for the property adding one for the null terminator.
nBufLen = trueLength + 1;
wszValue = new WCHAR[nBufLen];
}
if (NULL == wszValue)
{
WcaLog(LOGMSG_STANDARD, "Unable to determine the user interface level the MSI is being run with because we were unable to allocate storage for accessing the 'UILevel' property.");
return false;
}
memset(wszValue, L'\0', nBufLen * sizeof(WCHAR));
result = ::MsiGetPropertyW(hInstall, L"UILevel", wszValue, &trueLength);
if (ERROR_SUCCESS != result)
{
WcaLog(LOGMSG_STANDARD, "Unable to determine the user interface level the MSI is being run with, error code = '%lu'.", result);
delete [] wszValue;
return false;
}
if (0 == wcscmp(L"2", wszValue)) // INSTALLUILEVEL_NONE == 2
{
delete [] wszValue;
return true;
}
delete [] wszValue;
return false;
}
I believe I can work around this for now by passing the 'UILevel' property through WiX and checking for it that way in the C++, but I am curious what the problem here is as well.
I'm using Visual Studio/Visual C++ 2010 on Windows 7 with WiX 3.5.2519.
Thanks for any assistance you can provide!
Another way of making this simpler is to use the MsiEvaluateCondition function.
BOOL bUI = MsiEvaluateCondition(L"UILevel<3");
in C# using Microsoft.Deployment.WindowsIntaller (DTF) it's:
var uiLevel = session["UILevel"];
In C++ there's a sample at MsiGetProperty function:
UINT __stdcall MyCustomAction(MSIHANDLE hInstall)
{
TCHAR* szValueBuf = NULL;
DWORD cchValueBuf = 0;
UINT uiStat = MsiGetProperty(hInstall, TEXT("MyProperty"), TEXT(""), &cchValueBuf);
//cchValueBuf now contains the size of the property's string, without null termination
if (ERROR_MORE_DATA == uiStat)
{
++cchValueBuf; // add 1 for null termination
szValueBuf = new TCHAR[cchValueBuf];
if (szValueBuf)
{
uiStat = MsiGetProperty(hInstall, TEXT("MyProperty"), szValueBuf, &cchValueBuf);
}
}
if (ERROR_SUCCESS != uiStat)
{
if (szValueBuf != NULL)
delete[] szValueBuf;
return ERROR_INSTALL_FAILURE;
}
// custom action uses MyProperty
// ...
delete[] szValueBuf;
return ERROR_SUCCESS;
}
Thanks to #DanielGehriger, we figured out that the problem isn't with the code, but with the scheduling for the custom action. The UILevel MSI property is simply not available when running a deferred custom action (I found that the code worked correctly for a custom action scheduled for firstsequence). I have worked around this limitation by explicitly passing it on custom action data using WiX:
<CustomAction Id="CustomAction.SetProperty" Property="CustomActionCall"
Value="UILEVEL=[UILevel];" />
and then checking for this in the C++ with WcaIsPropertySet and WcaGetProperty. Note that the character case of the property name between square brackets matters here.