comshim

package module
v0.0.0-...-bbacf79 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 16, 2023 License: MIT Imports: 6 Imported by: 17

README

comshim GoDoc

The comshim package provides a mechanism for maintaining an initialized multi-threaded component object model apartment.

When working with mutli-threaded apartments, COM requires at least one thread to be initialized, otherwise COM-allocated resources may be released prematurely. This poses a challenge in Go, which can have many goroutines running in parallel with weak thread affinity.

The comshim package provides a solution to this problem by maintaining a single thread-locked goroutine that has been initialized for multi-threaded COM use via a call to CoIntializeEx. A reference counter is used to determine the ongoing need for the shim to stay in place. Once the counter reaches 0, the thread is released and COM may be deinitialized.

The comshim package is designed to allow COM-based libraries to hide the threading requirements of COM from the user. COM interfaces can be hidden behind idomatic Go structures that increment the counter with calls to NewType() and decrement the counter with calls to Type.Close(). To see how this is done, take a look at the WrapperUsage example.

Global Example

package main

import "github.com/scjalliance/comshim"

func main() {
	// This ensures that at least one thread maintains an initialized
	// multi-threaded COM apartment.
	comshim.Add(1)

	// After we're done using COM the thread will be released.
	defer comshim.Done()

	// Do COM things here
}

Wrapper Example

package main

import (
	"sync"

	"github.com/go-ole/go-ole"
	"github.com/go-ole/go-ole/oleutil"
	"github.com/scjalliance/comshim"
)

// Object wraps a COM interface in a way that is safe for multi-threaded access.
// In this example it wraps IUnknown.
type Object struct {
	m     sync.Mutex
	iface *ole.IUnknown
}

// NewObject creates a new object. Be sure to document the need to call Close().
func NewObject() (*Object, error) {
	comshim.Add(1)
	iunknown, err := oleutil.CreateObject("Excel.Application")
	if err != nil {
		comshim.Done()
		return nil, err
	}
	return &Object{iface: iunknown}, nil
}

// Close releases any resources used by the object.
func (o *Object) Close() {
	o.m.Lock()
	defer o.m.Unlock()
	if o.iface == nil {
		return // Already closed
	}
	o.iface.Release()
	o.iface = nil
	comshim.Done()
}

// Foo performs some action using the object's COM interface.
func (o *Object) Foo() {
	o.m.Lock()
	defer o.m.Unlock()

	// Make use of o.iface
}

func main() {
	obj1, err := NewObject() // Create an object
	if err != nil {
		panic(err)
	}
	defer obj1.Close() // Be sure to close the object when finished

	obj2, err := NewObject() // Create a second object
	if err != nil {
		panic(err)
	}
	defer obj2.Close() // Be sure to close it too

	// Work with obj1 and obj2
}

Documentation

Overview

Package comshim provides a mechanism for maintaining an initialized multi-threaded component object model apartment.

When working with mutli-threaded apartments, COM requires at least one thread to be initialized, otherwise COM-allocated resources may be released prematurely. This poses a challenge in Go, which can have many goroutines running in parallel with weak thread affinity.

The comshim package provides a solution to this problem by maintaining a single thread-locked goroutine that has been initialized for multi-threaded COM use via a call to CoIntializeEx. A reference counter is used to determine the ongoing need for the shim to stay in place. Once the counter reaches 0, the thread is released and COM may be deinitialized.

The comshim package is designed to allow COM-based libraries to hide the threading requirements of COM from the user. COM interfaces can be hidden behind idomatic Go structures that increment the counter with calls to NewType() and decrement the counter with calls to Type.Close(). To see how this is done, take a look at the WrapperUsage example.

Example (GlobalUsage)
package main

import (
	"github.com/scjalliance/comshim"
)

func main() {
	// This ensures that at least one thread maintains an initialized
	// multi-threaded COM apartment.
	comshim.Add(1)

	// After we're done using COM the thread will be released.
	defer comshim.Done()

	// Do COM things here
}
Output:

Example (WrapperUsage)
package main

import (
	"sync"

	"github.com/go-ole/go-ole"
	"github.com/go-ole/go-ole/oleutil"
	"github.com/scjalliance/comshim"
)

// Object wraps a COM interface in a way that is safe for multi-threaded access.
// In this example it wraps IUnknown.
type Object struct {
	m     sync.Mutex
	iface *ole.IUnknown
}

// NewObject creates a new object. Be sure to document the need to call Close().
func NewObject() (*Object, error) {
	comshim.Add(1)
	iunknown, err := oleutil.CreateObject("Excel.Application")
	if err != nil {
		comshim.Done()
		return nil, err
	}
	return &Object{iface: iunknown}, nil
}

// Close releases any resources used by the object.
func (o *Object) Close() {
	o.m.Lock()
	defer o.m.Unlock()
	if o.iface == nil {
		return // Already closed
	}
	o.iface.Release()
	o.iface = nil
	comshim.Done()
}

// Foo performs some action using the object's COM interface.
func (o *Object) Foo() {
	o.m.Lock()
	defer o.m.Unlock()

	// Make use of o.iface
}

func main() {
	obj1, err := NewObject() // Create an object
	if err != nil {
		panic(err)
	}
	defer obj1.Close() // Be sure to close the object when finished

	obj2, err := NewObject() // Create a second object
	if err != nil {
		panic(err)
	}
	defer obj2.Close() // Be sure to close it too

	// Work with obj1 and obj2
}
Output:

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNegativeCounter is returned when the internal counter of a shim drops
	// below zero. This may indicate that Done() has been called more than once
	// for the same object.
	ErrNegativeCounter = errors.New("component object model shim counter has dropped below zero")

	// ErrAlreadyInitialized is returned when a shim finds itself on a thread
	// that has already been initialized. This probably indicates that some
	// previous goroutine failed to lock the OS thread or failed to call
	// CoUninitialize when it should have.
	ErrAlreadyInitialized = errors.New("component object model shim thread has already been initialized")
)

Functions

func Add

func Add(delta int)

Add adds delta, which may be negative, to the counter of a global shim. As long as the counter is greater than zero, at least one thread is guaranteed to be initialized for mutli-threaded COM access.

If the counter becomes zero, the shim is released and COM resources may be released if there are no other threads that are still initialized.

If the counter goes negative, Add panics.

If the shim cannot be created for some reason, Add panics.

func Done

func Done()

Done decrements the counter of a global shim.

Types

type Counter

type Counter struct {
	// contains filtered or unexported fields
}

Counter wraps an int64 atomic counter in a way that provides proper byte alignment.

func (*Counter) Add

func (c *Counter) Add(delta int64) int64

Add will add the given delta value, which may be negative, to the atomic counter and return the new value.

func (*Counter) Value

func (c *Counter) Value() int64

Value returns the current value of the counter.

type Shim

type Shim struct {
	// contains filtered or unexported fields
}

Shim provides control of a thread-locked goroutine that has been initialized for use with a mulithreaded component object model apartment. This is used to ensure that at least one thread within a process maintains an initialized connection to COM, and thus prevents COM resources from being unloaded from that process.

Control is implemented through the use of a counter similar to a waitgroup. As long as the counter is greater than zero then the goroutine will remain in a blocked condition with its COM connection intact.

func New

func New() *Shim

New returns a new shim for keeping component object model resources allocated within a process.

func (*Shim) Add

func (s *Shim) Add(delta int)

Add adds delta, which may be negative, to the counter for the shim. As long as the counter is greater than zero, at least one thread is guaranteed to be initialized for mutli-threaded COM access.

If the counter becomes zero, the shim is released and COM resources may be released if there are no other threads that are still initialized.

If the counter goes negative, Add panics.

If the shim cannot be created for some reason, Add panics.

func (*Shim) Done

func (s *Shim) Done()

Done decrements the counter for the shim.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL