With some help from a coworker, I figured out how to do this. The message loop can be run on the same thread as the COM object using ApplicationContext and Application.Run. Other objects can communicate to the thread via Invoke on a dummy Control created on the message loop thread. using System; using System.Diagnostics; using System.Collections.Generic; using System.Text; using System.Threading; using System.Windows.Forms; namespace TestMessageLoopWindowsService { internal class TestMessageLoop { internal void Start(EventLog eventLog) { if (eventLog == null) throw new ArgumentNullException("eventLog"); this.eventLog = eventLog; Thread messageLoopThread = new Thread(MessageLoopThread); messageLoopThread.Name = "Message Loop Thread"; messageLoopThread.SetApartmentState(ApartmentState.STA); messageLoopThread.Start(); Thread invokeOnToMessageLoopThread = new Thread(Invoker); invokeOnToMessageLoopThread.Name = "Invoker"; invokeOnToMessageLoopThread.IsBackground = false; invokeOnToMessageLoopThread.Start(); } private EventLog eventLog; private void MessageLoopThread() { ApplicationContext context = new ApplicationContext(); control = new Control(); control.HandleCreated += new EventHandler(control_HandleCreated); control.CreateControl(); Application.Run(context); } private Control control; private void control_HandleCreated(object sender, EventArgs e) { okToInvoke = true; } private volatile bool okToInvoke; private void Invoker() { while (true) { if (okToInvoke) { EventHandler method = new EventHandler(InvokedOnMessageLoop); control.Invoke(method); } Thread.Sleep(1000); } } private void InvokedOnMessageLoop(object sender, EventArgs e) { eventLog.WriteEntry("InvokedOnMessageLoop runs on " + Thread.CurrentThread.Name); } } }