Skip to content

Instantly share code, notes, and snippets.

@ariankordi
Created July 13, 2025 01:29
Show Gist options
  • Save ariankordi/6e8a4ec77c87ee8e219370b744be3a4b to your computer and use it in GitHub Desktop.
Save ariankordi/6e8a4ec77c87ee8e219370b744be3a4b to your computer and use it in GitHub Desktop.
Mii thumbnailer for Windows PoC using the FFL-Testing Mii renderer server
/**
* \file FFLTestingThumbProvider.cpp
* \author Arian Kordi (https://github.com/ariankordi)
* \date 2025/07/12
*
* \brief Windows thumbnail provider for Mii data files using
* the FFL-Testing Mii renderer server: https://github.com/ariankordi/FFL-Testing
* \details Requires setting up and running on default port of 12346.
* Not ideal for real use. Should be considered a proof-of-concept/toy.
* https://github.com/ariankordi
*
* Build (MSVC/Visual Studio):
cl /LD /Fefflthumb.dll FFLTestingThumbProvider.cpp ole32.lib gdi32.lib ^
shlwapi.lib advapi32.lib ws2_32.lib shell32.lib ntdll.lib ^
/link -EXPORT:DllGetClassObject,PRIVATE -EXPORT:DllCanUnloadNow,PRIVATE ^
-EXPORT:DllRegisterServer,PRIVATE -EXPORT:DllUnregisterServer,PRIVATE
* Build (gcc/MinGW-w64):
x86_64-w64-mingw32-g++ -s -shared -o fflthumb.dll \
FFLTestingThumbProvider.cpp -lole32 -lgdi32 -lshlwapi \
-ladvapi32 -lws2_32 -luuid
* (Optionally, to exclude STL: gcc instead of g++, extra args:
* -nostdlib -lkernel32 -Wl,-entry,DllMain -fno-exceptions)
*
*
* Register for current user:
* regsvr32 fflthumb.dll (calls DllRegisterServer)
* Unregister:
* regsvr32 /u fflthumb.dll (calls DllUnregisterServer)
*/
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <initguid.h> // To allow building on MinGW.
#include <shlwapi.h> // SHDeleteKeyW, SHChangeNotify
#include <thumbcache.h> // IThumbnailProvider, WTS_ALPHATYPE
#include <shlobj.h>
#include <strsafe.h>
// #include <new>
#include <winsock2.h>
#include <ws2tcpip.h>
#ifdef _MSC_VER
// Export symbols for MSVC.
#pragma comment(linker, "/EXPORT:DllGetClassObject,PRIVATE")
#pragma comment(linker, "/EXPORT:DllCanUnloadNow,PRIVATE")
#pragma comment(linker, "/EXPORT:DllRegisterServer,PRIVATE")
#pragma comment(linker, "/EXPORT:DllUnregisterServer,PRIVATE")
#endif
/// Address and port of the renderer server.
#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT "12346"
/// Definition to enable showing a message box when any COM method is called.
/// This is useful for knowing when to inject a debugger into dllhost.exe.
// #define MESSAGE_BOX_DEBUG
/// If this is defined, a static buffer for the thumbnail
/// will be used instead of dynamically allocating from heap.
#define IMAGE_STATIC_BUF
/// Defines the maximum resolution.
/// Requests for thumbnails higher than this will use the max.
#define THUMBNAIL_MAX_CX 1024
/// The CLSID and title for our thumbnail provider.
#define THUMBNAIL_PROVIDER_CLSID L"{4C7CA1A0-C21C-4284-85DC-7AE838138FED}"
#define THUMBNAIL_PROVIDER_TITLE L"FFL-Testing Thumbnail Provider"
/// CLSID for the IThumbnailProvider shell extension.
/// See: https://learn.microsoft.com/en-us/windows/win32/shell/thumbnail-providers
#define SHELLEX_THUMBNAIL_CLSID L"ShellEx\\{E357FCCD-A995-4576-B01F-234630154E96}"
/// Macro to create a registry key string.
/// File extensions must include leading '.'
#define MAKE_EXT_KEY(ext) \
L"Software\\Classes\\" ext L"\\" SHELLEX_THUMBNAIL_CLSID
/// The registry keys for extensions this handler supports.
static constexpr const wchar_t* g_extensionKeyPaths[] = {
// Wii U/3DS (Ver3StoreData)
MAKE_EXT_KEY(L".ffsd"), // Wii U (FFLStoreData)
MAKE_EXT_KEY(L".cfsd"), // 3DS (CFLStoreData)
MAKE_EXT_KEY(L".3dsmii"), // Custom: 3DS (kazuki-4ys / CFLiPackedMiiDataOfficial)
// Also custom: "cfcd"/"ffcd" (https://github.com/HEYimHeroic/MiiDataFiles/blob/main/README.md#wii-u3ds-formats) - but not used by any tools
// Wii
MAKE_EXT_KEY(L".rsd"), // Wii (RFLStoreData) - Usually preferred for newer Wii games
MAKE_EXT_KEY(L".rcd"), // Wii (RFLCharData)
MAKE_EXT_KEY(L".mii"), // Custom: Usually Wii
// .mii is also used in Mario Golf on Switch for CharInfo
MAKE_EXT_KEY(L".miigx"), // Custom: Wii (SaveGame Manager GX / RFLCharData)
MAKE_EXT_KEY(L".mae"), // Custom: Wii (My Avatar Editor / RFLCharData)
// Switch
MAKE_EXT_KEY(L".charinfo"), // NintendoSDK NX Add-On (nn::mii::CharInfo)
MAKE_EXT_KEY(L".mnms"), // Custom: studio.mii.nintendo.com web editor
MAKE_EXT_KEY(L".nfcd"), // Custom: NintendoSDK (nn::mii::CoreData for import file)
MAKE_EXT_KEY(L".nfsd") // Custom: NintendoSDK (nn::mii::StoreData for database)
};
/// Global parsed CLSID object from g_thumbClsidString.
static CLSID g_thumbClsid;
/// Windows module handle saved from DllMain.
static HMODULE g_moduleHandle = nullptr;
// snippet below from: https://github.com/microsoft/CMake/blob/a5caf2fee0a42735b8f5f54e146da39099f1a8a6/Utilities/cmlibarchive/libarchive/archive_write_set_format_cpio_binary.c#L75
// snippet to define that a struct should not have alignment
#ifdef __GNUC__
#define PACKED(x) x __attribute__((packed))
#elif defined(_MSC_VER)
#define PACKED(x) __pragma(pack(push, 1)) x __pragma(pack(pop))
#endif
/// Packed request structure sent to the server.
/// See: https://github.com/ariankordi/FFL-Testing/blob/renderer-server-prototype/include/RenderRequest.h
/// Structure representing a render request, derived from
/// request query parameters by caller (web server).
PACKED(struct RenderRequest
{
unsigned char data[96]; // just a buffer that accounts for maximum size
unsigned short dataLength; // determines the mii data format
unsigned char modelFlag; // FFLModelType + nose flatten @ bit 4
// completely changes the response type:
unsigned char responseFormat; // indicates if response is gltf or tga
// note that arbitrary resolutions CRASH THE BACKEND
unsigned short resolution; // resolution for render buffer
// texture resolution controls mipmap enable (1 << 30)
short texResolution; // FFLResolution/u32, negative = mipmap
unsigned char viewType; // camera view (setViewTypeParams)
char resourceType; // FFLResourceType (default high/-1)
unsigned char shaderType; // custom ShaderType
unsigned char expression; // used if expressionFlag is all zeroes
unsigned int expressionFlag[3]; // casted to FFLAllExpressionFlag
// used for multiple expressions
// expressionFlag will only apply in gltf mode for now
short cameraRotate[3]; // converted deg2rad to vector
short modelRotate[3]; // same as above
unsigned char backgroundColor[4]; // passed to clearcolor
// controls scaling/anti-aliasing mode:
unsigned char aaMethod; // TODO: to be implemented perfectly
unsigned char drawStageMode; // custom DrawStageMode: opa, xlu, all
bool verifyCharInfo; // for FFLiVerifyCharInfoWithReason
bool verifyCRC16; // passed to pickupCharInfoFromData
bool lightEnable; // passed to IShader::bind()
char clothesColor; // favorite color, -1 for default
char pantsColor; // PantsColor, -1 = default shader
char bodyType; // BodyType, -1 = default for shader
char headwearIndex; // -1 = disabled.
char headwearColor; // -1
unsigned char instanceCount; // for instanceCountNewRender loop
unsigned char instanceRotationMode; // model, camera, TODO
short lightDirection[3]; // unset if all negative, TODO
unsigned char splitMode; // none (default), front, back, both};
});
/// Indicator at the beginning of a response that there was an error.
static const char* socketErrorPrefix = "ERROR: ";
#define ERROR_LEN sizeof(socketErrorPrefix) - 1 // Exclude terminator.
#define TGA_HEADER_SIZE 18
#ifdef IMAGE_STATIC_BUF
/// Static RGBA buffer for thumbnails.
/// NOTE: ASSUMES that only one thumbnail is made at a time (single-threaded)
static unsigned char gImageBuf[THUMBNAIL_MAX_CX * THUMBNAIL_MAX_CX * 4];
#endif
/// \brief Helper: connect, send request, receive TGA, return HBITMAP.
/// \param req[in] Filled RenderRequest.
/// \param phbmp[out] Receives the GDI bitmap.
/// \returns S_OK or error.
static HRESULT DoRenderRequest(const RenderRequest& req, HBITMAP* phbmp)
{
if (!phbmp) return E_POINTER;
*phbmp = nullptr;
WSADATA wsa = {};
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
return HRESULT_FROM_WIN32(WSAGetLastError());
struct addrinfo hints = {}, * res;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(SERVER_ADDR, SERVER_PORT, &hints, &res) != 0)
{
WSACleanup();
return E_FAIL;
}
SOCKET s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (s == INVALID_SOCKET)
{
freeaddrinfo(res);
WSACleanup();
return HRESULT_FROM_WIN32(WSAGetLastError());
}
if (connect(s, res->ai_addr, (int)res->ai_addrlen) != 0)
{
closesocket(s);
freeaddrinfo(res);
WSACleanup();
return HRESULT_FROM_WIN32(WSAGetLastError());
}
freeaddrinfo(res);
// Send the request.
int toSend = sizeof(req), sent = 0;
const char* buf = reinterpret_cast<const char*>(&req);
while (sent < toSend)
{
int n = send(s, buf + sent, toSend - sent, 0);
if (n <= 0)
{
closesocket(s);
WSACleanup();
return E_FAIL;
}
sent += n;
}
// Need to peek at the first 7 bytes to tell if this is an error.
char headerBuf[ERROR_LEN] = {};
int peeked = recv(s, headerBuf, ERROR_LEN, MSG_PEEK);
// STL: Can be replaced with memcmp.
if (peeked == ERROR_LEN &&
RtlCompareMemory(headerBuf, socketErrorPrefix, ERROR_LEN) == ERROR_LEN)
{
// socketErrorPrefix found in the message, meaning error.
closesocket(s);
WSACleanup();
return E_FAIL;
}
// Receive 18-byte TGA header.
unsigned char tgaHeader[TGA_HEADER_SIZE]{};
int rec = 0;
while (rec < TGA_HEADER_SIZE)
{
int n = recv(s, (char*)tgaHeader + rec, TGA_HEADER_SIZE - rec, 0);
if (n <= 0)
{
closesocket(s);
WSACleanup();
return E_FAIL;
}
rec += n;
}
// Parse little-endian width/height from TGA header.
unsigned short width = static_cast<unsigned short>(tgaHeader[12]) | static_cast<unsigned short>(tgaHeader[13]) << 8;
unsigned short height = static_cast<unsigned short>(tgaHeader[14]) | static_cast<unsigned short>(tgaHeader[15]) << 8;
if (!width || !height)
{
closesocket(s);
WSACleanup();
return E_FAIL;
}
// Allocate pixel buffer RGBA
size_t pixelsize = size_t(width) * height * 4; // Calculate RGBA byte size.
#ifndef IMAGE_STATIC_BUF
// STL: Can be replaced with malloc (+ HeapFree = free)
unsigned char* pixels = (unsigned char*)HeapAlloc(GetProcessHeap(), 0, pixelsize);
if (!pixels)
{
closesocket(s);
WSACleanup();
return E_OUTOFMEMORY;
}
#else
unsigned char* pixels = gImageBuf;
#endif
// Receive all pixel bytes
size_t total = 0;
while (total < pixelsize)
{
int n = recv(s, (char*)pixels + total, (int)(pixelsize - total), 0);
if (n <= 0)
{
#ifndef IMAGE_STATIC_BUF
HeapFree(GetProcessHeap(), 0, pixels);
#endif
closesocket(s);
WSACleanup();
return E_FAIL;
}
total += n;
}
closesocket(s);
WSACleanup();
// Create DIBSection (BGRA)
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
bmi.bmiHeader.biWidth = width;
bmi.bmiHeader.biHeight = -static_cast<int>(height);
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* mem = nullptr;
HBITMAP hbitmap = CreateDIBSection(nullptr, &bmi, DIB_RGB_COLORS, &mem, nullptr, 0);
if (!hbitmap)
{
#ifndef IMAGE_STATIC_BUF
HeapFree(GetProcessHeap(), 0, pixels);
#endif
return E_FAIL;
}
// Copy RGBA into BGRA (flip R/B)
BYTE* dst = (BYTE*)mem;
// for (size_t i = 0; i < pixelsize; i += 4)
// Ensure that a full 4-byte read is safe.
for (size_t i = 0; i + 3 < pixelsize; i += 4)
{
dst[i + 0] = pixels[i + 2]; // B <- R
dst[i + 1] = pixels[i + 1]; // G
dst[i + 2] = pixels[i + 0]; // R <- B
dst[i + 3] = pixels[i + 3]; // A
}
#ifndef IMAGE_STATIC_BUF
HeapFree(GetProcessHeap(), 0, pixels);
#endif
*phbmp = hbitmap;
return S_OK;
}
/// Helper to make a RenderRequest template.
static RenderRequest MakeRenderRequest()
{
RenderRequest req = {};
req.lightEnable = 1;
req.resourceType = -1;
req.bodyType = -1;
req.clothesColor = -1;
req.headwearIndex = -1;
req.headwearColor = -1;
req.backgroundColor[0] = 255;
req.backgroundColor[1] = 255;
req.backgroundColor[2] = 255;
req.backgroundColor[3] = 0;
req.lightDirection[0] = -1;
req.lightDirection[1] = -1;
req.lightDirection[2] = -1;
req.modelFlag = 1 << 0;
req.shaderType = 1; // SHADER_TYPE_SWITCH
req.viewType = 5; // VIEW_TYPE_NNMII_VARIABLEICONBODY
return req;
}
/**
* \brief Compute a simple hash of up to the first 64 bytes of the stream.
* \param pStream[in] The input IStream (must be seekable).
* \param outColor[out] RGB color packed as 0x00RRGGBB.
* \return HRESULT S_OK on success, error otherwise.
*/
/*
static HRESULT ComputeStreamColorHash(IStream* pStream, unsigned int& outColor)
{
// Seek to beginning
LARGE_INTEGER zero = {};
HRESULT hr = pStream->Seek(zero, STREAM_SEEK_SET, nullptr);
if (FAILED(hr)) return hr;
BYTE buffer[64] = {};
ULONG bytesRead = 0;
hr = pStream->Read(buffer, sizeof(buffer), &bytesRead);
if (FAILED(hr)) return hr;
// Simple sum-based hash.
unsigned int sum = 0;
for (ULONG i = 0; i < bytesRead; ++i)
{
sum += buffer[i];
}
// Derive R, G, B from sum
BYTE r = static_cast<BYTE>(sum & 0xFF);
BYTE g = static_cast<BYTE>((sum >> 8u) & 0xFF);
BYTE b = static_cast<BYTE>((sum >> 16u) & 0xFF);
outColor = (r << 16u) | (g << 8u) | b;
return S_OK;
}
*/
/**
* \brief Create a solid color BGRA HBITMAP of size cx x cx based on stream hash.
* \param pStream[in] The input IStream for hashing.
* \param cx[in] Desired thumbnail dimension (square).
* \param phbmp[out] Receives the created HBITMAP.
* \return HRESULT S_OK on success, error otherwise.
*/
/*
static HRESULT CreateSolidColorThumbnail(
IStream* pStream,
UINT cx,
HBITMAP* phbmp)
{
if (!pStream || !phbmp)
return E_POINTER;
unsigned int rgb = 0;
HRESULT hr = ComputeStreamColorHash(pStream, rgb);
if (FAILED(hr))
return hr;
// Define 32bpp BGRA DIB
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
bmi.bmiHeader.biWidth = static_cast<LONG>(cx);
bmi.bmiHeader.biHeight = -static_cast<LONG>(cx); ///< negative = top-down
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* pixels = nullptr;
HBITMAP hbm = CreateDIBSection(
nullptr,
&bmi,
DIB_RGB_COLORS,
&pixels,
nullptr,
0);
if (!hbm || !pixels)
return E_OUTOFMEMORY;
// Flip data from RGBA to BGRA.
BYTE* ptr = static_cast<BYTE*>(pixels);
BYTE blue = static_cast<BYTE>(rgb & 0xFF);
BYTE green = static_cast<BYTE>((rgb >> 8) & 0xFF);
BYTE red = static_cast<BYTE>((rgb >> 16) & 0xFF);
BYTE alpha = static_cast<BYTE>((rgb >> 24) & 0xFF);
for (UINT y = 0; y < cx; ++y)
{
for (UINT x = 0; x < cx; ++x)
{
ptr[0] = blue;
ptr[1] = green;
ptr[2] = red;
ptr[3] = alpha;
ptr += 4;
}
}
*phbmp = hbm;
return S_OK;
}
*/
/// COM object implementing IInitializeWithStream and IThumbnailProvider.
class ThumbnailProvider : public IInitializeWithStream, public IThumbnailProvider
{
private:
void ReleaseStream()
{
if (m_pStream)
{
m_pStream->Release();
m_pStream = nullptr;
}
}
public:
/// Constructor, initializes ref count.
ThumbnailProvider() : m_refCount(1), m_pStream(nullptr) {}
/// Destructor, releases stored stream.
/// NOTE: This has been ditched in favor of releasing
/// the stream in GetThumbnail, in order to have no destructors.
//~ThumbnailProvider()
//{
// ReleaseStream();
//}
// IUnknown
IFACEMETHODIMP QueryInterface(REFIID riid, void** ppvObject) override
{
if (!ppvObject)
return E_POINTER;
if (InlineIsEqualGUID(riid, IID_IUnknown) ||
InlineIsEqualGUID(riid, IID_IInitializeWithStream))
{
*ppvObject = static_cast<IInitializeWithStream*>(this);
}
else if (InlineIsEqualGUID(riid, IID_IThumbnailProvider))
{
*ppvObject = static_cast<IThumbnailProvider*>(this);
}
else
{
*ppvObject = nullptr;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
IFACEMETHODIMP_(ULONG) AddRef() override
{
return InterlockedIncrement(&m_refCount);
}
IFACEMETHODIMP_(ULONG) Release() override
{
ULONG count = InterlockedDecrement(&m_refCount);
//if (count == 0)
// delete this;
return count;
}
// IInitializeWithFile: Not implemented.
// IInitializeWithStream
IFACEMETHODIMP Initialize(IStream* pStream, DWORD /*grfMode*/) override
{
if (!pStream)
return E_INVALIDARG;
#if defined(_DEBUG) && defined(MESSAGE_BOX_DEBUG)
if (!IsDebuggerPresent())
{
MessageBoxW(nullptr,
L"IInitializeWithStream::Initialize called",
NULL,
MB_OK);
}
#endif
// Store and addref the stream
pStream->AddRef();
m_pStream = pStream;
return S_OK;
}
// IThumbnailProvider
IFACEMETHODIMP GetThumbnail(
UINT cx,
HBITMAP* phbmp,
WTS_ALPHATYPE* pdwAlpha) override
{
#if defined(_DEBUG) && defined(MESSAGE_BOX_DEBUG)
if (!IsDebuggerPresent())
{
MessageBoxW(nullptr,
L"IThumbnailProvider::GetThumbnail called",
NULL,
MB_OK);
}
#endif
if (!m_pStream || !phbmp || !pdwAlpha)
return E_POINTER;
*phbmp = nullptr;
*pdwAlpha = WTSAT_ARGB; // use alpha
// Check if the resolution exceeds the max, and clamp if needed.
if (cx > THUMBNAIL_MAX_CX)
cx = THUMBNAIL_MAX_CX;
// Build a RenderRequest template.
RenderRequest req = MakeRenderRequest();
// Read up to 96 bytes from the file into req.data.
LARGE_INTEGER zero = {};
HRESULT hr = m_pStream->Seek(zero, STREAM_SEEK_SET, nullptr);
if (FAILED(hr)) return hr;
ULONG bytesRead = 0;
hr = m_pStream->Read(req.data, sizeof(req.data), &bytesRead);
if (FAILED(hr)) return hr;
req.dataLength = static_cast<unsigned short>(bytesRead);
req.responseFormat = 0; // Expect TGA, RGBA
req.resolution = static_cast<unsigned short>(cx);
req.texResolution = static_cast<short>(cx);
// HRESULT hr = CreateSolidColorThumbnail(m_pStream, cx, phbmp);
hr = DoRenderRequest(req, phbmp);
if (FAILED(hr))
{
#ifdef _DEBUG
wchar_t msg[64];
swprintf_s(msg, _countof(msg),
L"GetThumbnail failed: HRESULT=0x%08X", (unsigned int)hr);
MessageBoxW(nullptr, msg, NULL, MB_OK);
#endif
return hr;
}
ReleaseStream();
return S_OK;
}
private:
LONG m_refCount; ///< COM reference count
IStream* m_pStream; ///< Stored file stream
};
static ThumbnailProvider g_ThumbnailProvider;
/// COM Class Factory for ThumbnailProvider.
class ClassFactory : public IClassFactory
{
public:
ClassFactory() : m_refCount(1) {}
// ~ClassFactory() {}
// IUnknown
IFACEMETHODIMP QueryInterface(REFIID riid, void** ppvObject) override
{
if (!ppvObject)
return E_POINTER;
if (InlineIsEqualGUID(riid, IID_IUnknown) ||
InlineIsEqualGUID(riid, IID_IClassFactory))
{
*ppvObject = static_cast<IClassFactory*>(this);
AddRef();
return S_OK;
}
*ppvObject = nullptr;
return E_NOINTERFACE;
}
IFACEMETHODIMP_(ULONG) AddRef() override
{
return InterlockedIncrement(&m_refCount);
}
IFACEMETHODIMP_(ULONG) Release() override
{
LONG count = InterlockedDecrement(&m_refCount);
//if (count == 0)
// delete this;
return count;
}
// IClassFactory
IFACEMETHODIMP CreateInstance(
IUnknown* pUnkOuter,
REFIID riid,
void** ppvObject) override
{
if (pUnkOuter != nullptr)
return CLASS_E_NOAGGREGATION;
ThumbnailProvider* pObj = &g_ThumbnailProvider;
//ThumbnailProvider* pObj = new (std::nothrow) ThumbnailProvider();
if (!pObj)
return E_OUTOFMEMORY;
HRESULT hr = pObj->QueryInterface(riid, ppvObject);
pObj->Release(); // balance ref from creation
return hr;
}
IFACEMETHODIMP LockServer(BOOL/* fLock*/) override
{
// Not tracking locks; always succeed
return S_OK;
}
private:
LONG m_refCount; ///< Factory ref count
// char _pad[4];
};
/// DLL entry point.
/// Parses the CLSID string into g_thumbClsid.
extern "C" BOOL APIENTRY DllMain(
HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID /*lpReserved*/)
{
HRESULT hr;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
g_moduleHandle = hModule;
DisableThreadLibraryCalls(hModule);
// Parse our CLSID once
hr = CLSIDFromString(THUMBNAIL_PROVIDER_CLSID,
&g_thumbClsid);
if (FAILED(hr)) DbgRaiseAssertionFailure();
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
static ClassFactory g_ClassFactory;
/// Standard COM export to retrieve our ClassFactory.
#ifdef _MSC_VER
_Check_return_
#endif
STDAPI DllGetClassObject(
_In_ REFCLSID rclsid,
_In_ REFIID riid,
_Outptr_ void** ppv)
{
if (!ppv)
return E_POINTER;
*ppv = nullptr;
if (!InlineIsEqualGUID(rclsid, g_thumbClsid))
return CLASS_E_CLASSNOTAVAILABLE;
// ClassFactory* factory = new (std::nothrow) ClassFactory();
ClassFactory* factory = &g_ClassFactory;
if (!factory)
return E_OUTOFMEMORY;
HRESULT hr = factory->QueryInterface(riid, ppv);
factory->Release();
return hr;
}
/// COM unload check. Always allow unload.
#ifdef _MSC_VER
__control_entrypoint(DllExport)
#endif
STDAPI DllCanUnloadNow(void)
{
return S_OK;
}
#define THUMBNAIL_PROVIDER_CLSID_KEY_PATH L"Software\\Classes\\CLSID\\" THUMBNAIL_PROVIDER_CLSID
#define THUMBNAIL_PROVIDER_APPID_KEY_PATH L"Software\\Classes\\AppID\\" THUMBNAIL_PROVIDER_CLSID
static inline DWORD InlineStringLenW(const wchar_t* str)
{
DWORD len = 0;
while (*str++) ++len;
return len;
}
/// Register the COM server and file associations under HKEY_CURRENT_USER.
STDAPI DllRegisterServer(void)
{
wchar_t modulePath[MAX_PATH] = {};
if (!GetModuleFileNameW(g_moduleHandle,
modulePath,
_countof(modulePath)))
{
return HRESULT_FROM_WIN32(GetLastError());
}
// Register CLSID key.
HKEY hClsidKey = nullptr;
if (RegCreateKeyExW(HKEY_CURRENT_USER,
THUMBNAIL_PROVIDER_CLSID_KEY_PATH,
0, nullptr,
REG_OPTION_NON_VOLATILE,
KEY_WRITE,
nullptr,
&hClsidKey,
nullptr) != ERROR_SUCCESS)
{
return E_FAIL;
}
// Set thumbnail provider title.
RegSetValueExW(hClsidKey,
nullptr,
0,
REG_SZ,
reinterpret_cast<const BYTE*>(THUMBNAIL_PROVIDER_TITLE),
static_cast<DWORD>(sizeof(THUMBNAIL_PROVIDER_TITLE)));
// InProcServer32 subkey.
HKEY hInproc = nullptr;
if (RegCreateKeyExW(hClsidKey,
L"InProcServer32",
0, nullptr,
REG_OPTION_NON_VOLATILE,
KEY_WRITE,
nullptr,
&hInproc,
nullptr) == ERROR_SUCCESS)
{
RegSetValueExW(hInproc,
nullptr,
0,
REG_SZ,
reinterpret_cast<const BYTE*>(modulePath),
static_cast<DWORD>(
(InlineStringLenW(modulePath) + 1) * (DWORD)sizeof(wchar_t)
));
RegSetValueExW(hInproc,
L"ThreadingModel",
0,
REG_SZ,
reinterpret_cast<const BYTE*>(L"Apartment"),
static_cast<DWORD>(sizeof(L"Apartment")));
RegCloseKey(hInproc);
}
#ifdef _DEBUG
// Register surrogate for debugging (dllhost.exe)
RegSetValueExW(hClsidKey,
L"AppID",
0,
REG_SZ,
reinterpret_cast<const BYTE*>(THUMBNAIL_PROVIDER_CLSID),
static_cast<DWORD>(sizeof(THUMBNAIL_PROVIDER_CLSID)));
HKEY hAppId = nullptr;
if (RegCreateKeyExW(HKEY_CURRENT_USER,
THUMBNAIL_PROVIDER_APPID_KEY_PATH,
0, nullptr,
REG_OPTION_NON_VOLATILE,
KEY_WRITE,
nullptr,
&hAppId,
nullptr) == ERROR_SUCCESS)
{
// Empty DllSurrogate: use dllhost.exe
RegSetValueExW(hAppId,
L"DllSurrogate",
0,
REG_SZ,
reinterpret_cast<const BYTE*>(L""),
sizeof(wchar_t));
RegCloseKey(hAppId);
}
#endif
RegCloseKey(hClsidKey);
// Register each extension under HKCU\Software\Classes.
for (const wchar_t* extKeyPath : g_extensionKeyPaths)
{
HKEY hExtKey = nullptr;
if (RegCreateKeyExW(HKEY_CURRENT_USER,
extKeyPath,
0, nullptr,
REG_OPTION_NON_VOLATILE,
KEY_WRITE,
nullptr,
&hExtKey,
nullptr) == ERROR_SUCCESS)
{
RegSetValueExW(hExtKey,
nullptr,
0,
REG_SZ,
reinterpret_cast<const BYTE*>(THUMBNAIL_PROVIDER_CLSID),
static_cast<DWORD>(sizeof(THUMBNAIL_PROVIDER_CLSID)));
RegCloseKey(hExtKey);
}
}
// Notify shell of association change.
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
return S_OK;
}
/// Unregister the COM server and file associations under HKEY_CURRENT_USER.
STDAPI DllUnregisterServer(void)
{
// TODO: Also consider deleting the file extension keys if
// they are not being used by any other program, to avoid
// polluting the user's system registry.
// Remove extension associations.
for (const wchar_t* extKeyPath : g_extensionKeyPaths)
{
RegDeleteTreeW(HKEY_CURRENT_USER, extKeyPath);
}
// Remove CLSID tree.
RegDeleteTreeW(HKEY_CURRENT_USER, THUMBNAIL_PROVIDER_CLSID_KEY_PATH);
#ifdef _DEBUG
// Remove AppID surrogate.
RegDeleteTreeW(HKEY_CURRENT_USER, THUMBNAIL_PROVIDER_APPID_KEY_PATH);
#endif
// Notify shell.
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
return S_OK;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment