6 ms·
COM Like a Bomb: Rust Outlook Add-in
- meibo 11mo agoI 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.
- snuxoll 11mo agoWhat 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).
- CrimsonCape 11mo ago> 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.
- pjmlp 11mo agoYes, provided you are using modern COM bindings introduced in .NET Core, alongside code generators.
- sedatk 11mo agoOr you could target .NET Framework 4.8 which is supported by all Windows OSes out of the box albeit quite outdated.
- Kwpolska 11mo agoTheir add-in seems quite simple, I imagine there would be no meaningful difference between using the classic .NET Framework 4.8 and .NET 10.
- merb 11mo agoYou 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.
- pjc50 11mo ago> 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.
- jlarocco 11mo agoI 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.
- pjmlp 11mo agoNot 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.
- ok123456 11mo agoUsing COM in Perl was pretty seamless back in its heyday.
- asveikau 11mo agoCOM 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.
- snuxoll 11mo agoAs 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.
- ptx 11mo agoCouldn'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.
- deleted 11mo ago[deleted]
- Kwpolska 11mo agoThey would need to know what a COM type library is in the first place.
- deleted 11mo ago[deleted]
- bri3d 11mo agoThis 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/5512e41b855ad24a29c7a80d99b500c786e0db15/connect.h#L86 https://github.com/microsoft/SampleNativeCOMAddin/blob/5512e... https://github.com/microsoft/SampleNativeCOMAddin/blob/5512e41b855ad24a29c7a80d99b500c786e0db15/Connect.cpp#L191 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.
- bigstrat2003 11mo ago> 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.
- piker 11mo agoIn 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.
- LegionMammal978 11mo agoA 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.
- rconti 11mo agoReference: Rage Against the Machine song "Calm Like a Bomb" https://www.youtube.com/watch?v=h2TLwwrLKbY https://www.youtube.com/watch?v=h2TLwwrLKbY
- JanneVee 11mo agoFun 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-class?view=msvc-170#remarks 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.
- hyperrail 11mo agoEric Lippert's "Complete Guide to BSTR Semantics" explains this well: https://ericlippert.com/2003/09/12/erics-complete-guide-to-bstr-semantics/ https://ericlippert.com/2003/09/12/erics-complete-guide-to-b...
- wunderwuzzi23 11mo agoIn 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-automate-anything-on-windows/ https://embracethered.com/blog/posts/2025/mcp-com-server-aut...
- comex 11mo agoFYI, 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.BSTR.html 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 https://github.com/microsoft/windows-rs/issues/3230
- garaetjjte 11mo agoI'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=15463 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>
- garaetjjte 11mo ago>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-developer-tools-use-type-libraries https://learn.microsoft.com/en-us/windows/win32/com/how-deve...
- piker 11mo agoI 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.
- moribvndvs 11mo ago> 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.