-
-
Save mstroeck/b96588fb536993f6b0ad to your computer and use it in GitHub Desktop.
Persistent toast notification in Win32 app with COM server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "stdafx.h" | |
#include "ToastActivator_h.h" | |
using namespace Microsoft::WRL; | |
using namespace ABI::Windows::UI::Notifications; | |
using namespace ABI::Windows::Data::Xml::Dom; | |
using namespace Windows::Foundation; | |
class DECLSPEC_UUID("BD8EC9B3-CAEB-465A-B5C0-2ABA5D5839D1") CToastActivator | |
WrlFinal : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, INotificationActivationCallback, FtmBase> | |
{ | |
public: | |
virtual HRESULT STDMETHODCALLTYPE Activate( | |
_In_ LPCWSTR /*appUserModelId*/, | |
_In_ LPCWSTR /*invokedArgs*/, | |
/*_In_reads_(dataCount)*/ const NOTIFICATION_USER_INPUT_DATA* /*data*/, | |
ULONG /*dataCount*/) override | |
{ | |
MessageBox(NULL, L"Toast activated", L"Toast Notofication", MB_OK); | |
} | |
//IUnknown methods are implemented here | |
HRESULT __stdcall QueryInterface( | |
REFIID riid, | |
void **ppObj) | |
{ | |
if (riid == IID_IUnknown) | |
{ | |
*ppObj = static_cast<INotificationActivationCallback*>(this); | |
AddRef(); | |
return S_OK; | |
} | |
if (riid == IID_INotificationActivationCallback) | |
{ | |
*ppObj = static_cast<INotificationActivationCallback*>(this); | |
AddRef(); | |
return S_OK; | |
} | |
*ppObj = NULL; | |
return E_NOINTERFACE; | |
} | |
ULONG __stdcall AddRef() | |
{ | |
return InterlockedIncrement(&m_nRefCount); | |
} | |
ULONG __stdcall Release() | |
{ | |
long nRefCount = 0; | |
nRefCount = InterlockedDecrement(&m_nRefCount); | |
if (nRefCount == 0) delete this; | |
return nRefCount; | |
} | |
CToastActivator() | |
{ | |
// | |
//constructor | |
// | |
m_nRefCount = 0; | |
} | |
~CToastActivator() | |
{ | |
} | |
private: | |
long m_nRefCount; | |
}; | |
// | |
// Main function | |
int WINAPI wWinMain(_In_ HINSTANCE /* hInstance */, _In_opt_ HINSTANCE /* hPrevInstance */, _In_ LPWSTR /* lpCmdLine */, _In_ int /* nCmdShow */) | |
{ | |
HRESULT hr = Initialize(RO_INIT_MULTITHREADED); | |
if (SUCCEEDED(hr)) | |
{ | |
Module<OutOfProc>::GetModule().RegisterObjects(); | |
DesktopToastsApp app; | |
hr = app.Initialize(); | |
if (SUCCEEDED(hr)) | |
{ | |
app.RunMessageLoop(); | |
} | |
Uninitialize(); | |
} | |
return SUCCEEDED(hr); | |
} | |
DesktopToastsApp::DesktopToastsApp() : _hwnd(nullptr), _hEdit(nullptr) | |
{ | |
} | |
DesktopToastsApp::~DesktopToastsApp() | |
{ | |
} | |
// In order to display toasts, a desktop application must have a shortcut on the Start menu. | |
// Also, an AppUserModelID must be set on that shortcut. | |
// The shortcut should be created as part of the installer. The following code shows how to create | |
// a shortcut and assign an AppUserModelID using Windows APIs. You must download and include the | |
// Windows API Code Pack for Microsoft .NET Framework for this code to function | |
// | |
// Included in this project is a wxs file that be used with the WiX toolkit | |
// to make an installer that creates the necessary shortcut. One or the other should be used. | |
HRESULT DesktopToastsApp::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\\Desktop Toasts App.lnk"); | |
hr = concatError == 0 ? S_OK : E_INVALIDARG; | |
if (SUCCEEDED(hr)) | |
{ | |
DWORD attributes = GetFileAttributes(shortcutPath); | |
bool fileExists = attributes < 0xFFFFFFF; | |
if (!fileExists) | |
{ | |
hr = InstallShortcut(shortcutPath); | |
} | |
else | |
{ | |
hr = S_FALSE; | |
} | |
} | |
} | |
return hr; | |
} | |
// Install the shortcut | |
HRESULT DesktopToastsApp::InstallShortcut(_In_z_ wchar_t *shortcutPath) | |
{ | |
wchar_t exePath[MAX_PATH]; | |
DWORD charWritten = GetModuleFileNameEx(GetCurrentProcess(), nullptr, exePath, ARRAYSIZE(exePath)); | |
HRESULT hr = charWritten > 0 ? S_OK : E_FAIL; | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IShellLink> shellLink; | |
hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); | |
if (SUCCEEDED(hr)) | |
{ | |
hr = shellLink->SetPath(exePath); | |
if (SUCCEEDED(hr)) | |
{ | |
hr = shellLink->SetArguments(L""); | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IPropertyStore> propertyStore; | |
hr = shellLink.As(&propertyStore); | |
if (SUCCEEDED(hr)) | |
{ | |
PROPVARIANT appIdPropVar; | |
hr = InitPropVariantFromString(AppId, &appIdPropVar); | |
if (SUCCEEDED(hr)) | |
{ | |
hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar); | |
if (SUCCEEDED(hr)) | |
{ | |
/* hr = propertyStore->Commit();*/ | |
PropVariantClear(&appIdPropVar); | |
appIdPropVar.vt = VT_CLSID; | |
appIdPropVar.puuid = const_cast<CLSID*>(&__uuidof(CToastActivator)); | |
hr = propertyStore->SetValue(PKEY_AppUserModel_ToastActivatorCLSID, appIdPropVar); | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IPersistFile> persistFile; | |
hr = shellLink.As(&persistFile); | |
if (SUCCEEDED(hr)) | |
{ | |
hr = persistFile->Save(shortcutPath, TRUE); | |
} | |
} | |
} | |
PropVariantClear(&appIdPropVar); | |
} | |
} | |
} | |
} | |
} | |
} | |
return hr; | |
} | |
// Prepare the main window | |
HRESULT DesktopToastsApp::Initialize() | |
{ | |
HRESULT hr = TryCreateShortcut(); | |
hr = DisplayToast(); | |
return hr; | |
} | |
// Standard message loop | |
void DesktopToastsApp::RunMessageLoop() | |
{ | |
MSG msg; | |
while (GetMessage(&msg, nullptr, 0, 0)) | |
{ | |
TranslateMessage(&msg); | |
DispatchMessage(&msg); | |
} | |
} | |
// Display the toast using classic COM. Note that is also possible to create and display the toast using the new C++ /ZW options (using handles, | |
// COM wrappers, etc.) | |
HRESULT DesktopToastsApp::DisplayToast() | |
{ | |
ComPtr<IToastNotificationManagerStatics> toastStatics; | |
HRESULT hr = GetActivationFactory(StringReferenceWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), &toastStatics); | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IXmlDocument> toastXml; | |
hr = CreateToastXml(toastStatics.Get(), &toastXml); | |
if (SUCCEEDED(hr)) | |
{ | |
hr = CreateToast(toastStatics.Get(), toastXml.Get()); | |
} | |
} | |
return hr; | |
} | |
// Create the toast XML from a template | |
HRESULT DesktopToastsApp::CreateToastXml(_In_ IToastNotificationManagerStatics *toastManager, _Outptr_ IXmlDocument** inputXml) | |
{ | |
// Retrieve the template XML | |
HRESULT hr = toastManager->GetTemplateContent(ToastTemplateType_ToastImageAndText04, inputXml); | |
if (SUCCEEDED(hr)) | |
{ | |
wchar_t *imagePath = _wfullpath(nullptr, L"toastImageAndText.png", MAX_PATH); | |
hr = imagePath != nullptr ? S_OK : HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); | |
if (SUCCEEDED(hr)) | |
{ | |
hr = SetImageSrc(imagePath, *inputXml); | |
if (SUCCEEDED(hr)) | |
{ | |
wchar_t* textValues[] = { | |
L"Line 1", | |
L"Line 2", | |
L"Line 3" | |
}; | |
UINT32 textLengths[] = {6, 6, 6}; | |
hr = SetTextValues(textValues, 3, textLengths, *inputXml); | |
} | |
} | |
} | |
return hr; | |
} | |
// Set the value of the "src" attribute of the "image" node | |
HRESULT DesktopToastsApp::SetImageSrc(_In_z_ wchar_t *imagePath, _In_ IXmlDocument *toastXml) | |
{ | |
wchar_t imageSrc[MAX_PATH] = L"file:///"; | |
HRESULT hr = StringCchCat(imageSrc, ARRAYSIZE(imageSrc), imagePath); | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IXmlNodeList> nodeList; | |
hr = toastXml->GetElementsByTagName(StringReferenceWrapper(L"image").Get(), &nodeList); | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IXmlNode> imageNode; | |
hr = nodeList->Item(0, &imageNode); | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IXmlNamedNodeMap> attributes; | |
hr = imageNode->get_Attributes(&attributes); | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IXmlNode> srcAttribute; | |
hr = attributes->GetNamedItem(StringReferenceWrapper(L"src").Get(), &srcAttribute); | |
if (SUCCEEDED(hr)) | |
{ | |
hr = SetNodeValueString(StringReferenceWrapper(imageSrc).Get(), srcAttribute.Get(), toastXml); | |
} | |
} | |
} | |
} | |
} | |
return hr; | |
} | |
// Set the values of each of the text nodes | |
HRESULT DesktopToastsApp::SetTextValues(_In_reads_(textValuesCount) wchar_t **textValues, _In_ UINT32 textValuesCount, _In_reads_(textValuesCount) UINT32 *textValuesLengths, _In_ IXmlDocument *toastXml) | |
{ | |
HRESULT hr = textValues != nullptr && textValuesCount > 0 ? S_OK : E_INVALIDARG; | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IXmlNodeList> nodeList; | |
hr = toastXml->GetElementsByTagName(StringReferenceWrapper(L"text").Get(), &nodeList); | |
if (SUCCEEDED(hr)) | |
{ | |
UINT32 nodeListLength; | |
hr = nodeList->get_Length(&nodeListLength); | |
if (SUCCEEDED(hr)) | |
{ | |
hr = textValuesCount <= nodeListLength ? S_OK : E_INVALIDARG; | |
if (SUCCEEDED(hr)) | |
{ | |
for (UINT32 i = 0; i < textValuesCount; i++) | |
{ | |
ComPtr<IXmlNode> textNode; | |
hr = nodeList->Item(i, &textNode); | |
if (SUCCEEDED(hr)) | |
{ | |
hr = SetNodeValueString(StringReferenceWrapper(textValues[i], textValuesLengths[i]).Get(), textNode.Get(), toastXml); | |
} | |
} | |
} | |
} | |
} | |
} | |
return hr; | |
} | |
HRESULT DesktopToastsApp::SetNodeValueString(_In_ HSTRING inputString, _In_ IXmlNode *node, _In_ IXmlDocument *xml) | |
{ | |
ComPtr<IXmlText> inputText; | |
HRESULT hr = xml->CreateTextNode(inputString, &inputText); | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IXmlNode> inputTextNode; | |
hr = inputText.As(&inputTextNode); | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IXmlNode> pAppendedChild; | |
hr = node->AppendChild(inputTextNode.Get(), &pAppendedChild); | |
} | |
} | |
return hr; | |
} | |
// Create and display the toast | |
HRESULT DesktopToastsApp::CreateToast(_In_ IToastNotificationManagerStatics *toastManager, _In_ IXmlDocument *xml) | |
{ | |
ComPtr<IToastNotifier> notifier; | |
HRESULT hr = toastManager->CreateToastNotifierWithId(StringReferenceWrapper(AppId).Get(), ¬ifier); | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IToastNotificationFactory> factory; | |
hr = GetActivationFactory(StringReferenceWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), &factory); | |
if (SUCCEEDED(hr)) | |
{ | |
ComPtr<IToastNotification> toast; | |
hr = factory->CreateToastNotification(xml, &toast); | |
if (SUCCEEDED(hr)) | |
{ | |
// Register the event handlers | |
EventRegistrationToken activatedToken, dismissedToken, failedToken; | |
ComPtr<ToastEventHandler> eventHandler(new ToastEventHandler(_hwnd, _hEdit)); | |
hr = toast->add_Activated(eventHandler.Get(), &activatedToken); | |
if (SUCCEEDED(hr)) | |
{ | |
hr = toast->add_Dismissed(eventHandler.Get(), &dismissedToken); | |
if (SUCCEEDED(hr)) | |
{ | |
hr = toast->add_Failed(eventHandler.Get(), &failedToken); | |
if (SUCCEEDED(hr)) | |
{ | |
hr = notifier->Show(toast.Get()); | |
} | |
} | |
} | |
} | |
} | |
} | |
return hr; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ToastActivator.idl : IDL source for ToastActivator | |
// | |
// This file will be processed by the MIDL tool to | |
// produce the type library (ToastActivator.tlb) and marshalling code. | |
import "oaidl.idl"; | |
import "ocidl.idl"; | |
typedef struct _NOTIFICATION_USER_INPUT_DATA | |
{ | |
LPCWSTR Key; | |
LPCWSTR Value; | |
} NOTIFICATION_USER_INPUT_DATA; | |
[ | |
object, | |
uuid("53E31837-6600-4A81-9395-75CFFE746F94"), | |
pointer_default(ref) | |
] | |
interface INotificationActivationCallback : IUnknown | |
{ | |
HRESULT Activate( | |
[in, string] LPCWSTR appUserModelId, | |
[in, string] LPCWSTR arguments, // arugments from the invoked button | |
[in, size_is(count), unique] const NOTIFICATION_USER_INPUT_DATA* data, // data from all the input elements in the XML | |
[in] ULONG count); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* this ALWAYS GENERATED file contains the definitions for the interfaces */ | |
/* File created by MIDL compiler version 8.00.0613 */ | |
/* at Tue Jan 19 08:44:07 2038 | |
*/ | |
/* Compiler settings for ToastActivator.idl: | |
Oicf, W1, Zp8, env=Win32 (32b run), target_arch=X86 8.00.0613 | |
protocol : dce , ms_ext, c_ext, robust | |
error checks: allocation ref bounds_check enum stub_data | |
VC __declspec() decoration level: | |
__declspec(uuid()), __declspec(selectany), __declspec(novtable) | |
DECLSPEC_UUID(), MIDL_INTERFACE() | |
*/ | |
/* @@MIDL_FILE_HEADING( ) */ | |
#pragma warning( disable: 4049 ) /* more than 64k source lines */ | |
/* verify that the <rpcndr.h> version is high enough to compile this file*/ | |
#ifndef __REQUIRED_RPCNDR_H_VERSION__ | |
#define __REQUIRED_RPCNDR_H_VERSION__ 475 | |
#endif | |
#include "rpc.h" | |
#include "rpcndr.h" | |
#ifndef __RPCNDR_H_VERSION__ | |
#error this stub requires an updated version of <rpcndr.h> | |
#endif /* __RPCNDR_H_VERSION__ */ | |
#ifndef COM_NO_WINDOWS_H | |
#include "windows.h" | |
#include "ole2.h" | |
#endif /*COM_NO_WINDOWS_H*/ | |
#ifndef __ToastActivator_h_h__ | |
#define __ToastActivator_h_h__ | |
#if defined(_MSC_VER) && (_MSC_VER >= 1020) | |
#pragma once | |
#endif | |
/* Forward Declarations */ | |
#ifndef __INotificationActivationCallback_FWD_DEFINED__ | |
#define __INotificationActivationCallback_FWD_DEFINED__ | |
typedef interface INotificationActivationCallback INotificationActivationCallback; | |
#endif /* __INotificationActivationCallback_FWD_DEFINED__ */ | |
/* header files for imported files */ | |
#include "oaidl.h" | |
#include "ocidl.h" | |
#ifdef __cplusplus | |
extern "C"{ | |
#endif | |
/* interface __MIDL_itf_ToastActivator_0000_0000 */ | |
/* [local] */ | |
typedef struct _NOTIFICATION_USER_INPUT_DATA | |
{ | |
LPCWSTR Key; | |
LPCWSTR Value; | |
} NOTIFICATION_USER_INPUT_DATA; | |
extern RPC_IF_HANDLE __MIDL_itf_ToastActivator_0000_0000_v0_0_c_ifspec; | |
extern RPC_IF_HANDLE __MIDL_itf_ToastActivator_0000_0000_v0_0_s_ifspec; | |
#ifndef __INotificationActivationCallback_INTERFACE_DEFINED__ | |
#define __INotificationActivationCallback_INTERFACE_DEFINED__ | |
/* interface INotificationActivationCallback */ | |
/* [ref][uuid][object] */ | |
EXTERN_C const IID IID_INotificationActivationCallback; | |
#if defined(__cplusplus) && !defined(CINTERFACE) | |
MIDL_INTERFACE("53E31837-6600-4A81-9395-75CFFE746F94") | |
INotificationActivationCallback : public IUnknown | |
{ | |
public: | |
virtual HRESULT STDMETHODCALLTYPE Activate( | |
/* [string][in] */ LPCWSTR appUserModelId, | |
/* [string][in] */ LPCWSTR arguments, | |
/* [unique][size_is][in] */ const NOTIFICATION_USER_INPUT_DATA *data, | |
/* [in] */ ULONG count) = 0; | |
}; | |
#else /* C style interface */ | |
typedef struct INotificationActivationCallbackVtbl | |
{ | |
BEGIN_INTERFACE | |
HRESULT ( STDMETHODCALLTYPE *QueryInterface )( | |
INotificationActivationCallback * This, | |
/* [in] */ REFIID riid, | |
/* [annotation][iid_is][out] */ | |
_COM_Outptr_ void **ppvObject); | |
ULONG ( STDMETHODCALLTYPE *AddRef )( | |
INotificationActivationCallback * This); | |
ULONG ( STDMETHODCALLTYPE *Release )( | |
INotificationActivationCallback * This); | |
HRESULT ( STDMETHODCALLTYPE *Activate )( | |
INotificationActivationCallback * This, | |
/* [string][in] */ LPCWSTR appUserModelId, | |
/* [string][in] */ LPCWSTR arguments, | |
/* [unique][size_is][in] */ const NOTIFICATION_USER_INPUT_DATA *data, | |
/* [in] */ ULONG count); | |
END_INTERFACE | |
} INotificationActivationCallbackVtbl; | |
interface INotificationActivationCallback | |
{ | |
CONST_VTBL struct INotificationActivationCallbackVtbl *lpVtbl; | |
}; | |
#ifdef COBJMACROS | |
#define INotificationActivationCallback_QueryInterface(This,riid,ppvObject) \ | |
( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) | |
#define INotificationActivationCallback_AddRef(This) \ | |
( (This)->lpVtbl -> AddRef(This) ) | |
#define INotificationActivationCallback_Release(This) \ | |
( (This)->lpVtbl -> Release(This) ) | |
#define INotificationActivationCallback_Activate(This,appUserModelId,arguments,data,count) \ | |
( (This)->lpVtbl -> Activate(This,appUserModelId,arguments,data,count) ) | |
#endif /* COBJMACROS */ | |
#endif /* C style interface */ | |
#endif /* __INotificationActivationCallback_INTERFACE_DEFINED__ */ | |
/* Additional Prototypes for ALL interfaces */ | |
/* end of Additional Prototypes */ | |
#ifdef __cplusplus | |
} | |
#endif | |
#endif | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* this ALWAYS GENERATED file contains the IIDs and CLSIDs */ | |
/* link this file in with the server and any clients */ | |
/* File created by MIDL compiler version 8.00.0613 */ | |
/* at Tue Jan 19 08:44:07 2038 | |
*/ | |
/* Compiler settings for ToastActivator.idl: | |
Oicf, W1, Zp8, env=Win32 (32b run), target_arch=X86 8.00.0613 | |
protocol : dce , ms_ext, c_ext, robust | |
error checks: allocation ref bounds_check enum stub_data | |
VC __declspec() decoration level: | |
__declspec(uuid()), __declspec(selectany), __declspec(novtable) | |
DECLSPEC_UUID(), MIDL_INTERFACE() | |
*/ | |
/* @@MIDL_FILE_HEADING( ) */ | |
#pragma warning( disable: 4049 ) /* more than 64k source lines */ | |
#ifdef __cplusplus | |
extern "C"{ | |
#endif | |
#include <rpc.h> | |
#include <rpcndr.h> | |
#ifdef _MIDL_USE_GUIDDEF_ | |
#ifndef INITGUID | |
#define INITGUID | |
#include <guiddef.h> | |
#undef INITGUID | |
#else | |
#include <guiddef.h> | |
#endif | |
#define MIDL_DEFINE_GUID(type,name,l,w1,w2,b1,b2,b3,b4,b5,b6,b7,b8) \ | |
DEFINE_GUID(name,l,w1,w2,b1,b2,b3,b4,b5,b6,b7,b8) | |
#else // !_MIDL_USE_GUIDDEF_ | |
#ifndef __IID_DEFINED__ | |
#define __IID_DEFINED__ | |
typedef struct _IID | |
{ | |
unsigned long x; | |
unsigned short s1; | |
unsigned short s2; | |
unsigned char c[8]; | |
} IID; | |
#endif // __IID_DEFINED__ | |
#ifndef CLSID_DEFINED | |
#define CLSID_DEFINED | |
typedef IID CLSID; | |
#endif // CLSID_DEFINED | |
#define MIDL_DEFINE_GUID(type,name,l,w1,w2,b1,b2,b3,b4,b5,b6,b7,b8) \ | |
const type name = {l,w1,w2,{b1,b2,b3,b4,b5,b6,b7,b8}} | |
#endif !_MIDL_USE_GUIDDEF_ | |
MIDL_DEFINE_GUID(IID, IID_INotificationActivationCallback,0x53E31837,0x6600,0x4A81,0x93,0x95,0x75,0xCF,0xFE,0x74,0x6F,0x94); | |
#undef MIDL_DEFINE_GUID | |
#ifdef __cplusplus | |
} | |
#endif | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment