As far as I understand, Windows Runtime is the new infrastructure through which Windows exposes its APIs. My question is simple: how can I use that from pure C code? I don't mind writing more code, I just want to understand how things link together.
Let's take for example the basic example Microsoft gives: https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/desktop-to-uwp-enhance. Specifically, "Modify a C++ Win32 project to use Windows Runtime APIs", there's an example that shows how to display a toast notification from an application. How do I translate that code to make use of it from a plain .c file?
I found some header files in C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\winrt
, for example windows.ui.notifications.h
I think might be useful, but I don't know how I am supposed to use the things in there. On MSDN, no article talks about pure C, only various managed languages and projections for C++.
Please, this is more of an academic question. I have successfully used COM from C in the past and was pretty okay with it, but for this, I can't find any mention or article about it online.
Thank you.
Edit Now I have some code that executes successfully (resulting HRESULTs are S_OK) but no toast is shown. Any idea how to debug this? What could be failing after all? I haven't implemented the COM activator, since I have a similar PowerShell script that works with basically the same thing I have written in C. I am stuck and lost, maybe someone can help.
#include <stdio.h>
#include <initguid.h>
#include <roapi.h>
#pragma comment(lib, "runtimeobject.lib")
#include <Windows.ui.notifications.h>
#include <winstring.h>
#include <shobjidl_core.h>
#include <propvarutil.h>
#include <propkey.h>
#include <Psapi.h>
// ABI.Windows.UI.Notifications.IToastNotificationManagerStatics
// 50ac103f-d235-4598-bbef-98fe4d1a3ad4
DEFINE_GUID(UIID_IToastNotificationManagerStatics,
0x50ac103f,
0xd235, 0x4598, 0xbb, 0xef,
0x98, 0xfe, 0x4d, 0x1a, 0x3a, 0xd4
);
// ABI.Windows.Data.Xml.Dom.IXmlDocument
// f7f3a506-1e87-42d6-bcfb-b8c809fa5494
DEFINE_GUID(UIID_IXmlDocument,
0xf7f3a506,
0x1e87, 0x42d6, 0xbc, 0xfb,
0xb8, 0xc8, 0x09, 0xfa, 0x54, 0x94
);
// ABI.Windows.Data.Xml.Dom.IXmlDocumentIO
// 6cd0e74e-ee65-4489-9ebf-ca43e87ba637
DEFINE_GUID(UIID_IXmlDocumentIO,
0x6cd0e74e,
0xee65, 0x4489, 0x9e, 0xbf,
0xca, 0x43, 0xe8, 0x7b, 0xa6, 0x37
);
// ABI.Windows.Notifications.IToastNotificationFactory
// 04124b20-82c6-4229-b109-fd9ed4662b53
DEFINE_GUID(UIID_IToastNotificationFactory,
0x04124b20,
0x82c6, 0x4229, 0xb1, 0x09,
0xfd, 0x9e, 0xd4, 0x66, 0x2b, 0x53
);
// CLSID_ShellLink
// 00021401-0000-0000-C000-000000000046
DEFINE_GUID(CLSID_ShellLink,
0x00021401,
0x0000, 0x0000, 0xc0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x46
);
// IShellLinkW
// 000214F9-0000-0000-C000-000000000046
DEFINE_GUID(IID_ShellLink,
0x000214f9,
0x0000, 0x0000, 0xc0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x46
);
// IPropertyStore
// 886d8eeb-8cf2-4446-8d02-cdba1dbdcf99
DEFINE_GUID(IID_IPropertyStore,
0x886d8eeb,
0x8cf2, 0x4446, 0x8d, 0x02,
0xcd, 0xba, 0x1d, 0xbd, 0xcf, 0x99
);
// IPersistFile
// 0000010b-0000-0000-C000-000000000046
DEFINE_GUID(IID_IPersistFile,
0x0000010b,
0x0000, 0x0000, 0xc0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x46
);
#define APP_ID L"valinet.thunderbirdtoasts"
#define APP_CLSID L"04f9ecea-f0da-4ea2-b2d7-acf208ae30a1"
// APP_UUID
// 04f9ecea-f0da-4ea2-b2d7-acf208ae30a1
/*
DEFINE_GUID(APP_UUID,
0x04f9ecea,
0xf0da, 0x4ea2, 0xb2, 0xd7,
0xac, 0xf2, 0x08, 0xae, 0x30, 0xa1
);
*/
inline HRESULT InitPropVariantFromString(_In_ PCWSTR psz, _Out_ PROPVARIANT* ppropvar)
{
HRESULT hr = psz != NULL ? S_OK : E_INVALIDARG; // Previous API behavior counter to the SAL requirement.
if (SUCCEEDED(hr))
{
SIZE_T const byteCount = (SIZE_T)((wcslen(psz) + 1) * sizeof(*psz));
V_UNION(ppropvar, pwszVal) = (PWSTR)(CoTaskMemAlloc(byteCount));
hr = V_UNION(ppropvar, pwszVal) ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr))
{
memcpy_s(V_UNION(ppropvar, pwszVal), byteCount, psz, byteCount);
V_VT(ppropvar) = VT_LPWSTR;
}
}
if (FAILED(hr))
{
PropVariantInit(ppropvar);
}
return hr;
}
HRESULT InstallShortcut(_In_z_ wchar_t* shortcutPath)
{
wchar_t exePath[MAX_PATH];
DWORD charWritten = GetModuleFileNameEx(
GetCurrentProcess(),
NULL,
exePath,
ARRAYSIZE(exePath)
);
HRESULT hr = charWritten > 0 ? S_OK : E_FAIL;
if (SUCCEEDED(hr))
{
IShellLink* shellLink = NULL;
hr = CoCreateInstance(
&CLSID_ShellLink,
NULL,
CLSCTX_INPROC_SERVER,
&IID_ShellLink,
&shellLink
);
if (SUCCEEDED(hr))
{
hr = shellLink->lpVtbl->SetPath(shellLink, exePath);
if (SUCCEEDED(hr))
{
hr = shellLink->lpVtbl->SetArguments(shellLink, L"");
if (SUCCEEDED(hr))
{
IPropertyStore* propertyStore;
shellLink->lpVtbl->QueryInterface(
shellLink,
&IID_IPropertyStore,
&propertyStore
);
if (SUCCEEDED(hr))
{
PROPVARIANT appIdPropVar;
hr = InitPropVariantFromString(APP_ID, &appIdPropVar);
if (SUCCEEDED(hr))
{
hr = propertyStore->lpVtbl->SetValue(propertyStore, &PKEY_AppUserModel_ID, &appIdPropVar);
if (SUCCEEDED(hr))
{
PROPVARIANT appClsIdPropVar;
hr = InitPropVariantFromString(APP_CLSID, &appClsIdPropVar);
if (SUCCEEDED(hr))
{
hr = propertyStore->lpVtbl->SetValue(propertyStore, &PKEY_AppUserModel_ToastActivatorCLSID, &appClsIdPropVar);
if (SUCCEEDED(hr))
{
hr = propertyStore->lpVtbl->Commit(propertyStore);
if (SUCCEEDED(hr))
{
IPersistFile* persistFile = NULL;
shellLink->lpVtbl->QueryInterface(
shellLink,
&IID_IPersistFile,
&persistFile
);
if (SUCCEEDED(hr))
{
hr = persistFile->lpVtbl->Save(persistFile, shortcutPath, TRUE);
}
}
}
PropVariantClear(&appClsIdPropVar);
}
}
PropVariantClear(&appIdPropVar);
}
}
}
}
}
}
return hr;
}
HRESULT TryCreateShortcut()
{
wchar_t shortcutPath[MAX_PATH];
DWORD charWritten = GetEnvironmentVariable(L"APPDATA", shortcutPath, MAX_PATH);
HRESULT hr = charWritten > 0 ? S_OK : E_INVALIDARG;
if (SUCCEEDED(hr))
{
errno_t concatError = wcscat_s(shortcutPath, ARRAYSIZE(shortcutPath), L"\\Microsoft\\Windows\\Start Menu\\Programs\\Thunderbird Toasts.lnk");
hr = concatError == 0 ? S_OK : E_INVALIDARG;
if (SUCCEEDED(hr))
{
DWORD attributes = GetFileAttributes(shortcutPath);
BOOL fileExists = attributes < 0xFFFFFFF;
if (!fileExists)
{
hr = InstallShortcut(shortcutPath); // See step 2.
}
else
{
hr = S_FALSE;
}
}
}
return hr;
}
HRESULT CreateXmlDocumentFromString(
const wchar_t* xmlString,
__x_ABI_CWindows_CData_CXml_CDom_CIXmlDocument** doc
)
{
HRESULT hr;
HSTRING_HEADER header_;
HSTRING IXmlDocumentHString;
hr = WindowsCreateStringReference(
RuntimeClass_Windows_Data_Xml_Dom_XmlDocument,
wcslen(RuntimeClass_Windows_Data_Xml_Dom_XmlDocument),
&header_,
&IXmlDocumentHString
);
if (FAILED(hr))
{
printf("WindowsCreateStringReference IXmlDocumentHString\n");
return hr;
}
if (IXmlDocumentHString == NULL)
{
return 1;
}
IInspectable* pInspectable;
hr = RoActivateInstance(IXmlDocumentHString, &pInspectable);
if (SUCCEEDED(hr))
{
hr = pInspectable->lpVtbl->QueryInterface(
pInspectable,
&UIID_IXmlDocument,
doc
);
pInspectable->lpVtbl->Release(pInspectable);
}
else
{
printf("RoActivateInstance IXmlDocumentHString\n");
return hr;
}
__x_ABI_CWindows_CData_CXml_CDom_CIXmlDocumentIO* docIO;
(*doc)->lpVtbl->QueryInterface(
(*doc),
&UIID_IXmlDocumentIO,
&docIO
);
if (FAILED(hr))
{
printf("QueryInterface IXmlDocumentIO\n");
return hr;
}
HSTRING XmlString;
hr = WindowsCreateStringReference(
xmlString,
wcslen(xmlString),
&header_,
&XmlString
);
if (FAILED(hr))
{
printf("WindowsCreateStringReference XmlString\n");
return hr;
}
if (XmlString == NULL)
{
return 1;
}
hr = docIO->lpVtbl->LoadXml(docIO, XmlString);
if (FAILED(hr))
{
printf("LoadXml IXmlDocumentIO\n");
return hr;
}
return hr;
}
int main()
{
HRESULT hr = RoInitialize(RO_INIT_MULTITHREADED);
if (FAILED(hr))
{
printf("RoInitialize\n");
return 0;
}
TryCreateShortcut();
HSTRING_HEADER header_;
HSTRING ToastNotificationManagerHString;
hr = WindowsCreateStringReference(
RuntimeClass_Windows_UI_Notifications_ToastNotificationManager,
wcslen(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager),
&header_,
&ToastNotificationManagerHString
);
if (FAILED(hr))
{
printf("WindowsCreateStringReference ToastNotificationManagerHString\n");
return 0;
}
if (ToastNotificationManagerHString == NULL)
{
return 0;
}
__x_ABI_CWindows_CUI_CNotifications_CIToastNotificationManagerStatics* toastStatics = NULL;
hr = RoGetActivationFactory(
ToastNotificationManagerHString,
&UIID_IToastNotificationManagerStatics,
(LPVOID*)&toastStatics
);
if (FAILED(hr))
{
printf("RoGetActivationFactory ToastNotificationManagerHString\n");
return 0;
}
__x_ABI_CWindows_CData_CXml_CDom_CIXmlDocument* inputXml = NULL;
hr = CreateXmlDocumentFromString(
L"<toast activationType=\"protocol\" launch=\"imsprevn://0\" duration=\"long\">"
L"<visual><binding template=\"ToastGeneric\"><text>text1</text><text>text2</text><text placement=\"attribution\">attr</text>"
L"</binding></visual><audio src=\"ms-winsoundevent:Notification.Mail\" loop=\"false\" /></toast>"
, &inputXml
);
if (FAILED(hr))
{
printf("CreateXmlDocumentFromString\n");
return 0;
}
HSTRING AppIdHString;
hr = WindowsCreateStringReference(
APP_ID,
wcslen(APP_ID),
&header_,
&AppIdHString
);
if (FAILED(hr))
{
printf("WindowsCreateStringReference AppIdHString\n");
return 0;
}
if (AppIdHString == NULL)
{
return 0;
}
__x_ABI_CWindows_CUI_CNotifications_CIToastNotifier* notifier;
hr = toastStatics->lpVtbl->CreateToastNotifierWithId(toastStatics, AppIdHString, ¬ifier);
if (FAILED(hr))
{
printf("CreateToastNotifier\n");
return 0;
}
HSTRING ToastNotificationHString;
hr = WindowsCreateStringReference(
RuntimeClass_Windows_UI_Notifications_ToastNotification,
wcslen(RuntimeClass_Windows_UI_Notifications_ToastNotification),
&header_,
&ToastNotificationHString
);
if (FAILED(hr))
{
printf("WindowsCreateStringReference ToastNotificationHString\n");
return 0;
}
if (ToastNotificationHString == NULL)
{
return 0;
}
__x_ABI_CWindows_CUI_CNotifications_CIToastNotificationFactory* notifFactory = NULL;
hr = RoGetActivationFactory(
ToastNotificationHString,
&UIID_IToastNotificationFactory,
(LPVOID*)¬ifFactory
);
if (FAILED(hr))
{
printf("RoGetActivationFactory ToastNotificationHString\n");
return 0;
}
__x_ABI_CWindows_CUI_CNotifications_CIToastNotification2* notif = NULL;
hr = notifFactory->lpVtbl->CreateToastNotification(notifFactory, inputXml, ¬if);
if (FAILED(hr))
{
printf("CreateToastNotification\n");
return 0;
}
hr = notif->lpVtbl->put_Tag(notif, AppIdHString);
if (FAILED(hr))
{
printf("put_Tag\n");
return 0;
}
hr = notif->lpVtbl->put_Group(notif, AppIdHString);
if (FAILED(hr))
{
printf("put_Group\n");
return 0;
}
hr = notifier->lpVtbl->Show(notifier, notif);
if (FAILED(hr))
{
printf("Show\n");
return 0;
}
printf("success\n");
return 0;
}
Here is the manifest file (IsWindows10OrGreater()
returns 1):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity
type="win32"
name="valinet.testapp"
version="1.2.3.4"
processorArchitecture="x86"
/>
<description>TestApp</description>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
</application>
</compatibility>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<!--
UAC settings:
- app should run at same integrity level as calling process
- app does not need to manipulate windows belonging to
higher-integrity-level processes
-->
<requestedExecutionLevel
level="asInvoker"
uiAccess="false"
/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
Here is the PowerShell script:
param ($appid, $action, $title, $text, $attr, $duration, $audio)
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
$template = @"
<toast activationType="protocol" launch="$action" duration="$duration">
<visual>
<binding template="ToastGeneric">
<text><![CDATA[$title]]></text>
<text><![CDATA[$text]]></text>
<text placement="attribution"><![CDATA[$attr]]></text>
</binding>
</visual>
<audio src="$audio" loop="false" />
</toast>
"@
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
$toast.Tag = $appid
$toast.Group = $appid
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appid).Show($toast)
#if defined(__cplusplus) && !defined(CINTERFACE)
and then on the else they define stuff like__x_ABI_CWindows_CFoundation_CIGetActivationFactory
. So, there are some C structs defined, even though they have criptic names, I think they should still be usable. Also, I think files in that folder are for an older API, and maybe the newer Windows Runtime is in cppwinrt... I am very confused, they have so many APIs... – PlainclothesmanShow
in the C implementation returns a success code. – Yachtsmanif SUCCEEDED(hr)
which supposedly reassure you that everything went fine, only in the end to end with 'success' but no actual result... Anyway, thank you for helping me. Do you know of anything else that might be at play here? – PlainclothesmanGet-StartApps
, but how can I check whether PowerShell registered a COM activator as well. Even if it did, it still doesn't make any sense to me why you need that when you are going to use a protocol, and even the docs say the COM activator is optional. – Plainclothesman