alt.hn

12/10/2025 at 3:10:36 PM

COM Like a Bomb: Rust Outlook Add-in

https://tritium.legal/blog/outlook

by piker

12/10/2025 at 4:58:19 PM

This is quite interesting: it's easy to blame the use of LLM to find the interface, but really this is a matter of needing to understand the COM calling conventions in order to interact with it.

I found the interface and a C++ sample in about two minutes of GitHub searching:

https://github.com/microsoft/SampleNativeCOMAddin/blob/5512e...

https://github.com/microsoft/SampleNativeCOMAddin/blob/5512e...

but I don't actually think this would have helped the Rust implementation; the authors already knew they wanted a BSTR and a BSTR*, they just didn't understand the COM conventions for BSTR ownership.

by bri3d

12/10/2025 at 10:17:54 PM

Every time I read an article on someone understanding COM from interfaces and dispatching, I think: reinventing Delphi, badly.

by vintagedave

12/11/2025 at 3:59:42 AM

COM is cross-language, though, and cross-process, and even cross-machine although not often used that way these days.

Life is definitely easier if you can restrict everything to being in the same language.

by antonvs

12/11/2025 at 4:37:01 PM

Delphi was designed to be COM-compatible, so the vtable layout was compatible, for example. Its interfaces, via the inbuilt interface keyword, use COM-compatible reference counting. It has inbuilt RTL types for handling a lot of common COM scenarios. It did this back in the 90s and remains extremely useful for COM still today.

Then late 2010s, C++Builder (its sister product) dropped ATL to DAX -- Delphi ActiveX aka COM -- and using COM from C++ uses the same inbuilt support, including keyword suggestions and RTL types. It's not quite as clean since it uses language bridging to do so, but it's still a lot nicer than normal C++ and COM.

Seeing someone do COM from first principles in 2025 is jarring.

by vintagedave

12/12/2025 at 8:43:10 AM

You mean, like Microsoft themselves?

.NET COM support was never as nice, with the RCW/CCW layer, now they have redoned it for modern .NET Core, still you need some knowledge how to use it from C++ to fully master it.

Then there is CsWinRT, which is supposed to be the runtime portion of .NET Native, which to this day has enough bugs and not as easy to use as it was .NET Native.

Finally, on the C++ side it has been a wasteland of frameworks, since MFC there have been multiple attempts, and when they finally had something close to C++ Builder with C++/CX, an internal team managed to sell to their managers the idea to kill C++/CX and replace it with C++/WinRT.

Nowadays C++/WinRT is sold as the way to do COM and WinRT, it is actually in maintenance, stuck in C++17, those folks moved on to the windows-rs project mentioned on the article, and the usuability story sucks.

Editing IDL files without any kind of code completion or syntax highlighting, non-existing tooling since COM was introduced, manually merging the generated C++ code into the ongoing project.

To complement your last sentence, seeing Microsoft employees push COM from first principles in 2025 is jarring.

by pjmlp

12/14/2025 at 10:35:33 PM

OLE at least looked easier to me in Assembler than in C++. Back in the day.

by actionfromafar

12/11/2025 at 6:18:46 PM

Oh I see. Python, Ruby, and various other high level languages, including of course the MS languages, have pretty seamless integration as well, although not at the level of direct binary compatibility. I imagine they just use wrappers.

by antonvs

12/12/2025 at 8:44:02 AM

Not C++, it has been a battlefield of frameworks, each reboot with its own set of sharp edges.

by pjmlp

12/11/2025 at 12:09:32 AM

I feel that way about most of frontend development since I was a teenager playing with Delphi 7.

by throwup238

12/10/2025 at 7:48:39 PM

> it's easy to blame the use of LLM to find the interface, but really this is a matter of needing to understand the COM calling conventions in order to interact with it.

Sure, but I think that this perfectly illustrates why LLMs are not good at programming (and may well never get good): they don't actually understand anything. An LLM is fundamentally incapable of going "this is COM so let me make sure that the function signature matches the calling conventions", it just generates something based on the code it has seen before.

I don't blame the authors for reaching for an LLM given that Microsoft has removed the C++ example code (seriously, what's up with that nonsense?). But it does very nicely highlight why LLMs are such a bad tool.

by bigstrat2003

12/10/2025 at 9:07:55 PM

In defense of the LLM here: learning COM from scratch given its lack of accessible documentation would have forced us to reach for C# for this minor project.

The LLM gave us an initial boost of productivity and (false) confidence that enabled us to get at the problem with Rust. While the LLM's output was flawed, using it did actually cause us to learn a lot about COM by allowing us to even getting started. That somewhat flies in the face of a lot of the "tech debt" criticisms levied at LLMs (including by me). Yes, we accumulated a bit of debt while working on the project, but were in this case able to pay it off before shipping and it gave us the leverage we needed to approach this problem using pure Rust.

by piker

12/11/2025 at 1:57:09 PM

You might actually get that desired behavior through reasoning, or if the model was reinforced for coding workflows involving COM, or at least enough stack diversity for the model to encounter the need to develop this capability.

In the case of LLMs with reasoning, they might pull this off because reasoning is in fact a search in the direction of extra considerations that improve its performance on the task. This is measured by the verifier during reasoning training, which the LLM learns to emulate during inference hence improved performance.

As for RL coding training, the difference can be slightly blurry since reasoning is also done with RL, but for coding models specifically they also discover additional considerations, or even recipes, through self play against a code execution environment. If that environment includes COM and the training data has COM-related tasks, then the process has a chance to discover the behavior you described and reinforce it during training increasing its likelihood during actual coding.

LLMs are not really just autocomplete engines. Perhaps the first few layers or for base models can be seen as such, but as you introduce instruct and reinforcement tuning LLMs build progressively higher levels of conceptual abstractions from words to sentences to tasks like CNNs learn basic geometric features then composing those into face parts and so on.

by omneity

12/10/2025 at 4:49:08 PM

I don't like Windows, but I've always thought COM was pretty cool. It's a nightmare using it directly from low level languages like C++ and Rust, though. It's a perfect place to use code generation or metaprogramming.

In Python, Ruby and the Microsoft languages COM objects integrate seamlessly into the language as instances of the built-in class types.

Also, there's a fairly straightfoward conversion from C# to C++ signatures, which becomes apparent after you see a few of them. It might be explicitly spelled out in the docs somewhere.

by jlarocco

12/10/2025 at 5:35:41 PM

COM is basically just reference counting and interfaces. Also, the HRESULT type tries to give some structure to 32 bit error codes.

I remember a few years back hearing hate about COM and I didn't feel like they understood what it was.

I think the legit criticisms include:

* It relies heavily on function pointers (virtual calls) so this has performance costs. Also constantly checking those HRESULTs for errors, I guess, gives you a lot more branching than exceptions.

* The idea of registration, polluting the Windows registry. These days this part is pretty optional.

by asveikau

12/10/2025 at 6:30:07 PM

As somebody who's been, for whatever reason, toying around with writing a COM-style ABI layer in Rust, there's a lot of good ideas in there and I think a lot of the hatred comes from the DLL hell that was spawned by registration; along with the, unfortunately necessary, boilerplate.

Virtual dispatch absolutely has an overhead, but absolutely nobody in their right mind should be using COM interfaces in a critical section of code. When we're talking things like UI elements, HTTP clients, whatever, the overhead of an indirect call is negligible compared to the time spent inside a function.

The one thing I'm personally trying to see if there's any room for improvement on in a clean slate design, is error handling / HRESULT values. Exceptions get abused for flow control and stack unwinding is expensive, so even if there was a sane way to implement cross-language exception handling it's a non starter. But HRESULT leads to IErrorInfo, ISupportErrorInfo, thread local state SetErrorInfo/GetErrorInfo, which is a whole extra bunch of fun to deal with.

There's the option of going the GObject and AppKit route, using an out parameter for an Error type - but you have to worry about freeing/releasing this in your language bindings or risk leaking memory.

by snuxoll

12/10/2025 at 7:03:44 PM

> Virtual dispatch absolutely has an overhead, but absolutely nobody in their right mind should be using COM interfaces in a critical section of code.

I could definitely be wrong, but I think C++ style "virtual dispatch" (ie, following two pointers instead of one to get to your function) doesn't really cost anything anymore, except for the extra pointers taking up cache space.

Don't all of the Windows DirectX gaming interfaces use COM? And isn't AAA gaming performance critical?

by dleary

12/10/2025 at 7:49:49 PM

> Don't all of the Windows DirectX gaming interfaces use COM? And isn't AAA gaming performance critical?

Yes, on both counts. You will also, on average, be making fewer calls to ID3D12CommandQueue methods than one would think - you'd submit an entire vertex buffer for a model (or specific components of it that need the same pipeline state, at least) at once, allocate larger pools of memory on the GPU and directly write textures to it, etc.

This is the entire design behind D3D12, Vulkan, and Metal - more direct interaction with the GPU, batching submission, and caching command buffers for reuse.

When I'm talking about "critical sections" of code, I mean anything with a tight loop where you can reasonably expect to pin a CPU core with work. For a game, this would be things like creating vertex buffers, which is why all three major API's take these as bare pointers to data structures in memory instead of requiring discrete calls to create and populate them.

by snuxoll

12/12/2025 at 8:49:10 AM

Registration free COM has existed since Windows XP, if I get my timeline right without bothering to look it up.

All modern Windows APIs introduced since Vista have been COM, classical Win32 C APIs are seldom introduced nowadays.

Certainly current Windows 11 performance problems have nothing to do with using COM all over the place, rather Webwidgets instead of native code, hiring people that apparently never did Windows programming, that apparently do AI driven coding.

Ah, macOS and iDevices driver model is equally based in COM like design, one would expect drivers to be something where performance matters.

Then there is XPC, Android IPC, and one could consider D-BUS as well, if it was more widely adopted across the GNU/Linux world.

by pjmlp

12/12/2025 at 9:25:28 PM

You are absolutely right on all counts, although XPC/Binder/D-Bus aren't really something to compare against the core of COM (the ABI model), and I think many Windows developers would have some unkind things to say about DCOM.

by snuxoll

12/10/2025 at 7:15:42 PM

WinRT is certainly not a "clean slate design", but still a useful comparison to see where Microsoft themselves iterated on the COM design with decades of hindsight.

by WorldMaker

12/11/2025 at 7:36:02 AM

Pity that the great tooling that came with it is now gone, alongside UWP.

WinRT tooling on Win32 side is a bad joke.

I almost lost count of how many COM frameworks have come and gone since OLE 1.0 days.

by pjmlp

12/10/2025 at 6:56:36 PM

> COM is basically just reference counting and interfaces. > I remember a few years back hearing hate about COM and I didn't feel like they understood what it was.

Even in "core" COM there's also marshaling, the whole client/server IPC model, and apartments.

And, I think most people encounter COM with one of its friends attached (like in this case, OLE/Automation in the form of IDispatch), which adds an additional layer of complexity on top.

Honestly I think that COM is really nice, though. If they'd come up with some kind of user-friendly naming scheme instead of UUIDs, I don't even think it would get that much hate. It feels to me that 90% of the dislike for COM is the mental overhead of seeing and dealing with UUIDs when getting started.

Once you get past that part, it's really fast to do pretty complex stuff in; compared to the other things people have come up with like dbus or local gRPC and so on, it works really well for coordinating extensibility and lots of independent processes that need to work together.

by bri3d

12/11/2025 at 1:25:20 PM

Even the UUIDs aren't bad, they're a reasonable solution to Zooko's triangle. You can't globally assign names.

by pjc50

12/11/2025 at 10:59:16 PM

Yeah, I've often thought about what I'd do instead and there's no legitimate alternative. It might help developers feel better if they had some kind of "friendly name" functionality (ie - if registrations in the Registry had a package-identifier style string alongside), but that also wouldn't have flown when COM was invented and resources overall were much more scarce than they are today.

by bri3d

12/12/2025 at 9:38:06 PM

While they're not "the same", classic COM (or OLE? the whole history is a mess) did actually have ProgIDs, and WinRT introduces proper "classes" and namespaces (having given up global registration for everything but system provided API's) with proper "names" (you can even query them at runtime with IInspectable::GetRuntimeClassName).

Microsoft tried to do a lot with COM when they first released it, it wasn't just a solution for having a stable cross-language ABI, it was a way to share component libraries across multiple applications on a system, and a whole lot more.

> but that also wouldn't have flown when COM was invented and resources overall were much more scarce than they are today.

And this ultimately is the paradox of COM. There were good ideas, but given Microsoft's (mostly kept) promise of keeping old software working the bad ones have remained baked in.

by snuxoll

12/10/2025 at 7:25:07 PM

You might have been hearing some of that hate from me. I definitely don't understand COM, but I've had to use it once or twice. It's pretty far outside what I normally work on, which is all high-level garbage collected languages. I don't know if that's even the right dimension to distinguish it. I couldn't figure out how to use COM or what it's purpose was.

The task was some automated jobs doing MS word automation. This all happened about 20 years ago. I never did figure out how to get it to stop leaking memory after a couple days of searching. I think I just had the process restart periodically.

Compared to what I was accustomed to COM seemed weird and just unnecessarily difficult to work with. I was a lot less experienced then, but I haven't touched COM since. I still don't know what the intent of COM is or where it's documented, and nor have I tried to figure it out. But it's colored my impression of COM ever since.

I think there may be a lot of people like me. They had to do some COM thing because it was the only way to accomplish a task, and just didn't understand. They randomly poked it until it kind of worked, and swore never to touch it again.

by recursive

12/10/2025 at 7:44:26 PM

> I still don't know what the intent of COM is

COM is an ABI (application binary interface). You have two programs, compiled in different languages with different memory management strategies, potentially years apart. You want them to communicate. You either

-1 use a Foreign Function Interface (FFI) provided to those languages -2 serialize/deserialize data and send it over some channel like a socket

(2) is how the internet works so we've taken to doing it that way for many different systems, even if they don't need it. (1) is how operating systems work and how the kernel and other subsystems are exposed to user space.

The problem with FFI is that it's pretty barebones. You can move bytes and call functions, but there's no standard way of composing those bytes and function calls into higher level constructs like you use in OOP languages.

COM is a standard for defining that FFI layer using OOP patterns. Programs export objects which have well defined interfaces. There's a root interface all objects implement called "Unknown", and you can find out if an object supports another interface by calling `queryInterface()` with the id of a desired interface (all interfaces have a globally unique ID). You can make sure the object doesn't lose its data out of nowhere by calling `addRef()` to bump its reference count, and `release()` to decrement it (thus removing any ambiguity over memory management, for the most part - see TFA for an example where that fails).

> where it's documented

https://learn.microsoft.com/en-us/windows/win32/com/the-comp...

by duped

12/10/2025 at 9:00:14 PM

> You have two programs, compiled in different languages with different memory management strategies, potentially years ap

Sometimes they are even the same language. Windows has a few problems that I haven't seen in the Unix world, such as: each DLL potentially having an incompatible implementation of malloc, where allocating using malloc(3) in one DLL then freeing it with free(3) in another being a crash.

by asveikau

12/12/2025 at 9:47:15 PM

> where allocating using malloc(3) in one DLL then freeing it with free(3) in another being a crash.

This can still happen all the time on UNIX systems. glibc's malloc implementation is a fine general purpose allocator, but there's plenty of times where you want to bring in tcmalloc, jemalloc, etc. Of course, you hope that various libraries will resolve to your implementation of choice when the linker wires everything up, but they can opt not to just as easily.

by snuxoll

12/13/2025 at 12:39:50 AM

No actually, this doesn't happen the same way on modern Unix. The way symbol resolution works is just not the same. A library asking for an extern called "malloc" will get the same malloc. To use those other allocators, you would typically give them a different symbol name, or make the whole process use the new one.

A dll import on Windows explicitly calls for the DLL by name. You could have some DLLs explicitly ask for a different version of the Visual Studio runtime, or with different threading settings, release vs debug etc., and a C extern asking for simply the name "malloc", no other details, will resolve to that, possibly incompatible with another DLL in the same process despite the compiler's perspective of it just being extern void *malloc(size_t) and no other detail, no other decoration, rename of the symbol etc.. there might be a rarely used symbol versioning pragma to accomplish similar on a modern gcc/clang/elf setup but it's not the way anybody does this.

I would argue that the modern Unix way, with these limitations, is better, by the way. Maybe some older Unix in the early days of shared libraries, early 90s or so, tried what Windows does, I don't know. But it's not common today.

by asveikau

12/13/2025 at 1:47:36 AM

> No actually, this doesn't happen the same way on modern Unix. The way symbol resolution works is just not the same. A library asking for an extern called "malloc" will get the same malloc. To use those other allocators, you would typically give them a different symbol name, or make the whole process use the new one.

This is, yes, the behavior of both the ELF specification as well as the GNU linker.

I'm not here to get into semantics of symbol namespaces and resolution though, I can just as easily link a static jemalloc into an arbitrary ELF shared object and use it inside for every allocation and not give a damn about what the global malloc() symbol points to. There's a half dozen other ways I can have a local malloc() symbol as well instead of having the linker bind the global one.

Which, is the entire point I'm trying to make. Is this a bigger problem on Windows versus UNIX-like platforms due to the way runtime linker support is handled? Yes. Is it entirely possible to have the same issue, however? Yes, absolutely.

by snuxoll

12/13/2025 at 5:47:39 PM

In about 27 years of using Linux and BSD I don't think I've seen it once. If you work professionally in C on Windows it is a practical concern, an everyday occurrence.

Another absurdly common issue is passing a FILE * across a DLL boundary. It is highly unlikely to work. I used to have to train new hires not to do this and tell partner teams working on C APIs to include I/o abstractions that don't involve FILE*, which would illicit a response as if I'm an alien.

by asveikau

12/11/2025 at 7:39:59 AM

Because C standard library isn't part of the OS.

Outside UNIX, the C standard library is a responsibility of the C compiler vendor, not the OS.

Nowadays Windows might seem the odd one, however 30 years ago the operating system was more diverse.

You will also find similar issues on dynamic libraries in mainframes/micros from IBM and Unisys, still being sold.

by pjmlp

12/11/2025 at 3:21:15 PM

Yeah I know the reasons for this, I'm just saying it's not usual coming from currently dominant unix-like systems.

by asveikau

12/11/2025 at 12:54:54 AM

> I couldn't figure out how to use COM or what it's purpose was.

A shorter version than the other reply:

COM allows you to have a reference counted object with callable virtual methods. You can also ask for different virtual methods at runtime (QueryInterface).

Some of the use cases include: maybe those methods are implemented in a completely different programming language that you are using, for example I think one of the historical ones is JavaScript or vbscript interacting with C++. It standardizes the virtual calls in such a way that you can throw in such an abstraction. And since reference counting happens via a virtual call, memory allocation is also up to the callee. Another historical use case is to have the calls be handled in a different process.

by asveikau

12/10/2025 at 6:39:09 PM

I'd say COM is also run-time type safe casting, and importantly the reference counting is uniform which might help writing wrappers for dynamic and garbage collected languages.

I'm still not sure that it brings a lot to the table for ordinary application development.

by jstimpfle

12/10/2025 at 6:56:30 PM

It's been a while since I've written it professionally, but I felt the fact that it has consistent idioms and conventions helped me be somewhat more productive writing C++. In the vast landscape of C++ features it winds up making some decisions for you. You can use whatever you want within your component but the COM interfaces dictate how you talk to outside.

by asveikau

12/10/2025 at 4:53:07 PM

Not if using Delphi or C++ Builder.

For whatever reason all attempts to make COM easier to use in Visual C++, keep being sabotaged by internal teams.

It is like Windows team feels like it is a manhood test to use such low level tooling.

by pjmlp

12/10/2025 at 4:54:49 PM

Using COM in Perl was pretty seamless back in its heyday.

by ok123456

12/10/2025 at 4:45:59 PM

> But using C# required us to contemplate whether and which dotnet runtime our client supported. Or did we need to ship our own? Isn't this just a small launcher stub? This was just too much complexity outside of our wheelhouse to put between our product and the user. This is not to say that the C# approach isn't valid. It is just that our limited understanding of that ecosystem and its requirements counseled against shipping it as a primary entry point into our application.

You should be able to compile a relatively small, trimmed, standalone, AOT compiled library that uses native interop. (Correct me if i'm wrong, dotnet users). Then there would be no dependency on the framework.

by CrimsonCape

12/11/2025 at 1:23:31 PM

> You should be able to compile a relatively small, trimmed, standalone, AOT compiled library

Yes-ish. We do AOT at work on a fairly large app and keep tripping over corners. Admittedly we don't use COM. I believe if you know the objects you are using upfront then code generation will take care of this for you. The other options are:

- self-contained: this just means "compiler puts a copy of the runtime alongside your executable". Works fine, at the cost of tens of megabytes

- self-contained single file: the above, but the runtime is zipped into the executable. May unpack into a temporary directory behind the scenes. Slightly easier to handle, minor startup time cost.

by pjc50

12/10/2025 at 5:14:42 PM

Or you could target .NET Framework 4.8 which is supported by all Windows OSes out of the box albeit quite outdated.

by sedatk

12/10/2025 at 5:47:42 PM

Their add-in seems quite simple, I imagine there would be no meaningful difference between using the classic .NET Framework 4.8 and .NET 10.

by Kwpolska

12/10/2025 at 4:54:38 PM

Yes, provided you are using modern COM bindings introduced in .NET Core, alongside code generators.

by pjmlp

12/10/2025 at 7:53:07 PM

You can only use .net 4.8 when you create an outlook add-in.

I mean yes you can build it with native interop and aot. But then you would loose the .net benefits as well.

by merb

12/10/2025 at 4:30:44 PM

I will say that I'm surprised no other LLM picked this up, since the issue should be somewhat evident to people familiar with C++ and how COM works. COM APIs cannot represent "owned" strings.

Still better than whatever JS rats nest they came up with for the new Outlook.

by meibo

12/10/2025 at 6:34:33 PM

What do you mean by "owned" strings?

WinRT, which is ultimately just an evolution of COM, has HSTRING which can own the data inside it (as well as contain a reference to an existing chunk of memory with fast-pass strings).

by snuxoll

12/10/2025 at 4:58:54 PM

A lot of these automatic marshalling systems (in this case, windows-rs) can be annoyingly unintuitive or opaque in how they handle subtler details of memory ownership, character sets, how to allocate and free objects, etc. And then it's made worse by documentation that only gives the output of one marshalling system (in this case, .NET) that's different from the one you're using, so you have to translate it both backwards and forwards. I guess this is mainly a consequence of COM trying to be all things to all people, being used by both unmanaged and managed code.

by LegionMammal978

12/10/2025 at 6:29:39 PM

Fun fact about BSTR, it uses memory before the string pointer to store the length.

From the CComBSTR documentation from microsoft: "The CComBSTR class is a wrapper for BSTRs, which are length-prefixed strings. The length is stored as an integer at the memory location preceding the data in the string. A BSTR is null-terminated after the last counted character but may also contain null characters embedded within the string. The string length is determined by the character count, not the first null character." https://learn.microsoft.com/en-us/cpp/atl/reference/ccombstr...

From the book ATL internals that I read about 24 years ago.

"Minor Rant on BSTRs, Embedded NUL Characters in Strings, and Life in General From the book ATL internals that i read about 24 years ago.

The compiler considers the types BSTR and OLECHAR* to be synonymous. In fact, the BSTR symbol is simply a typedef for OLECHAR. For example, from wtypes.h: typedef / [wire_marshal] / OLECHAR __RPC_FAR BSTR;

This is more than somewhat brain damaged. An arbitrary BSTR is not an OLECHAR, and an arbitrary OLECHAR is not a BSTR. One is often misled on this regard because frequently a BSTR works just fine as an OLECHAR *.

STDMETHODIMP SomeClass::put_Name (LPCOLESTR pName) ; BSTR bstrInput = ... pObj->put_Name (bstrInput) ; // This works just fine... usually SysFreeString (bstrInput) ;

In the previous example, because the bstrInput argument is defined to be a BSTR, it can contain embedded NUL characters within the string. The put_Name method, which expects a LPCOLESTR (a NUL-character-terminated string), will probably save only the characters preceding the first embedded NUL character. In other words, it will cut the string short."

I wont link to the pirated edition which is never than the one I read.

So if there is code in outlook that relies on the preceding bytes being the string length it can be the cause of the memory corruption. It would require a sesssion in the debugger to figure it out.

by JanneVee

12/11/2025 at 3:02:18 PM

>In fact, despite many minutes of bona fide web searching, I was unable to locate the C++ signature for IRibbonExtensibility.

Probably because the COM "intended" way is to generate them from type library. Type library for these interfaces is embedded in Office MSO.DLL. You can use oleview.exe from Windows SDK to convert them to IDL syntax. This yields such signature:

    HRESULT GetCustomUI(
                            [in] BSTR RibbonID, 
                            [out, retval] BSTR* RibbonXml);
And then you can use MIDL tool to generate C headers:

    DECLSPEC_XFGVIRT(IRibbonExtensibility, GetCustomUI)
            /* [helpcontext][id] */ HRESULT ( STDMETHODCALLTYPE *GetCustomUI )( 
                IRibbonExtensibility * This,
                /* [in] */ BSTR RibbonID,
                /* [retval][out] */ BSTR *RibbonXml);
https://learn.microsoft.com/en-us/windows/win32/com/how-deve...

by garaetjjte

12/11/2025 at 8:44:02 PM

I actually stumbled upon that toward the end of the exercise, but couldn't figure out where `oleview.exe` landed after I installed the SDK. I only spent a couple of minutes, and at that point I was only looking to confirm what I already knew, but this will be the approach if/when we extend it.

by piker

12/10/2025 at 4:53:18 PM

Couldn't the correct function signatures be generated from the COM type library? Using an LLM for this is clearly not a good fit, as the article demonstrates.

by ptx

12/10/2025 at 5:48:07 PM

They would need to know what a COM type library is in the first place.

by Kwpolska

12/13/2025 at 5:03:06 AM

> Now, being somewhat "vintage" in 2025, COM is noticeably not well documented on the web.

To be fair, they were not in 2005 or any time before or since either. I think I had an outdated book by Don Box and a giant win32 reference book and muddled my way through. The inconsistent behavior and random crashes are also pretty much what I remember.

by moribvndvs

12/11/2025 at 3:31:08 AM

In case some of you find it entertaining. When MCP came out I had a flashback to COM/DCOM days, like IDispatch and list/tools.

So, I built an MCP server that can host any COM server. :)

Now, AI can launch and work on Excel, Outlook and even resurrect Internet Explorer.

https://embracethered.com/blog/posts/2025/mcp-com-server-aut...

by wunderwuzzi23

12/11/2025 at 7:58:51 AM

FYI, I believe your updated signature is still incorrect. You have:

    unsafe fn GetCustomUI(&self, _ribbon_id: *const BSTR, out: *mut BSTR) -> HRESULT {}
But as linked in bri3d's post, the original C++ signature is:

    STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR* RibbonXml);
It really is true that the second parameter is a pointer to BSTR and the first is not. This difference is because the second parameter is an out parameter.

Ultimately, I think windows-rs is at fault here for confusing API design. The BSTR type that it defines is fundamentally different from the BSTR type in C++. The Rust BSTR has a destructor and is always owned, whereas the C++ BSTR is just a typedef for a raw pointer which may be considered owned or borrowed depending on the context. It's not like C++ doesn't support destructors; this particular type just doesn't use them.

It makes sense for Rust bindings to define a safe wrapper type with a destructor. But if I were designing the bindings, I would have given the wrapper a different name from the original type to make the difference in semantics more obvious.

The Rust BSTR type is still ABI-compatible with the C++ one (because it's repr(transparent)), so it can be valid to use it in FFI definitions, but only if that BSTR happens to be owned (like with the second parameter).

A more thorough wrapper for BSTR would provide a safe borrowed type in addition to the owned type, like what &str is to String. But it appears that windows-rs doesn't provide such a type. However, windows-rs does provide an unsafe type which can be used for the purpose. Confusingly, this type is also named BSTR, but it's defined in the windows-sys crate instead of windows-strings. This BSTR is like the C++ BSTR, just an alias for a raw pointer:

https://docs.rs/windows-sys/latest/windows_sys/core/type.BST...

You should probably use that type for the _ribbon_id parameter. Or you could just manually write out `*const u16`. But not `*const BSTR`, which is a pointer to a pointer. `*const BSTR` happens to be the same size as `BSTR` so it doesn't cause problems for an unused parameter, but it would break if you tried to use it.

Which probably doesn't matter to your application. But since you published a "correct signature for future LLMs", you should probably fix it.

See also this issue report I found (not exactly on point but related):

https://github.com/microsoft/windows-rs/issues/3230

by comex

12/11/2025 at 3:36:05 PM

I'm not sure if second argument is correct either. When assigning through *mut pointer, Drop will be called for previous value, but there's no guarantee that this value is zero-initialized. (according to https://devblogs.microsoft.com/oldnewthing/20091231-00/?p=15... callee is required to initialize all output arguments, which implies that caller is not required to). It should be represented as &mut std::mem::MaybeUninit<BSTR>

by garaetjjte

12/11/2025 at 7:56:41 PM

I think you're right. My mistake. Maybe the best option is `&mut windows_sys::core::BSTR` (using the unsafe BSTR type I mentioned), since that way the same BSTR type can be used for the two arguments. Or `*mut BSTR`, since windows-rs itself seems to prefer raw pointers for whatever reason, though I found crates using windows-rs that seem to successfully use references.

I am slightly suspicious that Raymond Chen might have been confused. The link in that post has the text "forget to set the output pointer to NULL", but in the linked post (the original link is broken but it's at [1]), the implementation actually set the output pointer to a garbage value rather than leaving it untouched. I wonder what the marshalling implementation actually looks like…

But at any rate, treating the out pointer as uninitialized is definitely the safe option. I'm not 100% sure whether it can legitimately point to non-null, but if it does point to non-null, then that value is definitely garbage rather than something that needs freeing.

[1] https://devblogs.microsoft.com/oldnewthing/20091007-00/?p=16...

by comex

12/11/2025 at 10:23:10 PM

>I think you're right. My mistake.

I didn't disagree with you, I just wanted to point another issue.

Actually *mut BSTR (owned) is also acceptable, iff you remember to use std::ptr::write instead of normal assignment.

> I'm not 100% sure whether it can legitimately point to non-null

Note that in none of the examples on this and other posts (like https://devblogs.microsoft.com/oldnewthing/20040326-00/?p=40...) output value is initialized, so it will be whatever is lying on the stack.

by garaetjjte

12/11/2025 at 11:12:36 PM

These are all great points, and I will update the blog post to reflect them in the morning.

I believe this approach can work while retaining the most apparently-idiomatic mapping. What do you guys think?

impl IRibbonExtensibility_Impl for Addin_Impl {

    unsafe fn GetCustomUI(&self, _ribbon_id: BSTR, out: *mut BSTR) -> HRESULT {

        log("GetCustomUI called()");

        std::mem::forget(_ribbon_id);

        if out.is_null() {

            return windows::Win32::Foundation::E_POINTER;
        }

        unsafe {

            std::ptr::write(out, BSTR::from(RIBBON_XML));
        }

        S_OK
    }

}

by piker

12/13/2025 at 2:24:07 AM

Looks fine to me, if you're avoiding wrappers like ManuallyDrop/MaybeUninit.

by garaetjjte

12/13/2025 at 10:24:30 AM

Actually the `windows-rs` team weighed in:

impl IRibbonExtensibility_Impl for Addin_Impl {

    unsafe fn GetCustomUI(

        &self,

        _ribbon_id: windows::core::Ref<BSTR>,

        out: windows::core::OutRef<BSTR>,

    ) -> HRESULT {

        log("GetCustomUI called()");

        if out.is_null() || out.write(BSTR::from(RIBBON_XML)).is_err() {

            return E_POINTER;

        };

        S_OK

    }
}

https://github.com/microsoft/windows-rs/issues/3832

Thanks for pushing on the issue! I've updated the blog post for GetCustomUI.

by piker

12/11/2025 at 11:15:58 PM

This is excellent. Thank you, and that signature did give me pause.

by piker