Skip to content

Instantly share code, notes, and snippets.

@mgspross
Last active December 18, 2020 01:02
Show Gist options
  • Save mgspross/dfadc21f16db2a56ed27 to your computer and use it in GitHub Desktop.
Save mgspross/dfadc21f16db2a56ed27 to your computer and use it in GitHub Desktop.
A proof-of-concept for defining COM interfaces in Go using go-ole, structs, and function-type fields
package main
import (
"fmt"
"github.com/mattn/go-ole"
"github.com/mattn/go-ole/oleutil"
"log"
"reflect"
"runtime"
)
// COM "interface" definition for MSXML2.DomDocument.
//
// Each method/property is declared with a field with the same name as the method or property,
// and the field's type is set to a function type that matches the method/property's call signature (using Go types)
//
// The `ole` tag is used to specify whether a given field represents a method or a property for objects of this type
//
type DomDocument struct {
Load func(file string) bool `ole:"method"`
LoadXml func(xml string) bool `ole:"method"`
Xml func() string `ole:"get"`
Save func(file string) error `ole:"method"`
DocumentElement func() *DomElement `ole:"get"`
}
type DomElement struct {
NodeName func() string `ole:"get"`
Text func() string `ole:"get"`
SetText func(text string) `ole:"set" ole.name:"Text"`
Xml func() string `ole:"get"`
SelectSingleNode func(name string) *DomElement `ole:"method"`
}
func main() {
doc := DomDocument{}
CreateOleObject("MSXML2.DomDocument.6.0", &doc)
if !doc.LoadXml("<root><child>Test</child></root>") {
log.Fatal("Could not load XML")
}
log.Println("XML:", doc.Xml())
// Successful test
err := doc.Save("test.xml")
if err != nil {
log.Print(err)
}
// Purposely fails to demonstrate returning error instead of panic
err = doc.Save("invalid-file-name-<#!?@>")
if err != nil {
log.Print(err.(*ole.OleError).Description())
}
doc2 := DomDocument{}
CreateOleObject("MSXML2.DomDocument.6.0", &doc2)
if !doc2.Load("test.xml") {
log.Fatal("Could not load doucment test.xml")
}
log.Println("XML:", doc2.Xml())
log.Println("DocumentElement.Name", doc2.DocumentElement().NodeName())
child := doc2.DocumentElement().SelectSingleNode("does_not_exist")
if child == nil {
log.Println("child node 'does_not_exist' not found in document")
}
child = doc2.DocumentElement().SelectSingleNode("child")
log.Println("child.NodeName:", child.NodeName())
log.Println("child.Text:", child.Text())
log.Println("child.Xml:", child.Xml())
child.SetText("foobar")
log.Println("Modified XML:", doc2.Xml())
}
// Instantiates a COM object and populates a template struct
// defining an "interface" for the object (via function-type fields
// corresponding to each method or property that needs to be accessed.
//
// The populated struct can then be used to call methods and properties
// in a natural way (syntatically it is identical to calling methods defined on a struct).
//
// This is a proof-of-concept, and more would need to done to make this fully generic.
// For example, supporting functions that return structs, by applying this function
// to them as well, so that COM objects that return other COM objects will continue to work
// seamlessly.
//
// Another question is how to define/handle errors in method calls. One possibility
// is to panic by default, but return an error struct if the corresponding field
// in the template for the method includes an error struct as one of its function type's return type(s).
// Additionally, this has the problem that there is no way to call Release() on the underlying
// COM object (I am thinking of how best to deal with this). We might have to require
// storing an *ole.IDispatch field in the template struct (and checking that it is present),
// in order to support this.
func CreateOleObject(progid string, template interface{}) {
ole.CoInitialize(0)
unk, err := oleutil.CreateObject(progid)
if err != nil {
panic(err)
}
disp := unk.MustQueryInterface(ole.IID_IDispatch)
unk.Release()
createOleWrapper(disp, template)
}
func createOleWrapper(disp *ole.IDispatch, template interface{}) {
val := reflect.ValueOf(template)
if val.Kind() != reflect.Ptr {
panic("val must be a pointer to a struct")
}
val = val.Elem()
if val.Kind() != reflect.Struct {
panic("val must be a struct")
}
for i := 0; i < val.NumField(); i++ {
field := val.Type().Field(i)
if field.Anonymous || field.Type.Kind() != reflect.Func {
continue
}
var callfunc interface{}
switch field.Tag.Get("ole") {
case "get":
callfunc = oleutil.GetProperty
case "set":
callfunc = oleutil.PutProperty
case "method":
callfunc = oleutil.CallMethod
default:
continue
}
name := field.Name
olename := field.Tag.Get("ole.name")
if olename > "" {
name = olename
}
validateInvokeFunc(val.Type().Name(), field.Name, field.Type)
f := reflect.MakeFunc(field.Type, makeInvokeFunc(disp, callfunc, name, field.Type))
val.FieldByName(field.Name).Set(f)
}
}
func validateInvokeFunc(structname string, fieldname string, functype reflect.Type) {
if functype.NumOut() > 2 {
panic(fmt.Errorf("Error in OLE wrapper struct %s (invalid method signature <%s> for field '%s'): Method must return 0, 1, or 2 values", structname, functype, fieldname))
}
if functype.NumOut() == 2 {
// function type must return (T, error) [in that order]
valid := !isErrorType(functype.Out(0)) && isErrorType(functype.Out(1))
if !valid {
panic(fmt.Errorf("Error in OLE wrapper struct %s (invalid method signature <%s> for field '%s'): Method has 2 return values. They must be in the form (T, error), where T is any non-error type", structname, functype, fieldname))
}
}
}
func makeInvokeFunc(disp *ole.IDispatch, callfunc interface{}, name string, functype reflect.Type) func([]reflect.Value) []reflect.Value {
return func(in []reflect.Value) []reflect.Value {
results := make([]reflect.Value, 0)
f := reflect.ValueOf(callfunc)
params := make([]reflect.Value, 0)
params = append(params, reflect.ValueOf(disp))
params = append(params, reflect.ValueOf(name))
params = append(params, in...)
out := f.Call(params)
err := out[1].Interface()
v := out[0].Interface().(*ole.VARIANT)
if v != nil {
// Not sure this is a good idea, particularly for *ole.IDispatch results...
runtime.SetFinalizer(v, func(v *ole.VARIANT) { ole.VariantClear(v) })
}
// If the function in the template struct returns at least one value, and the first
// value is *not* an error type, populate the return value into that return value
if functype.NumOut() > 0 && !isErrorType(functype.Out(0)) {
// nil/null/empty case - return zero-value for all of these
if v == nil || v.VT == ole.VT_NULL || v.VT == ole.VT_EMPTY || (v.VT == ole.VT_DISPATCH && v.Val == 0) {
results = append(results, reflect.Zero(functype.Out(0)))
} else if v.VT == ole.VT_DISPATCH {
rdisp := v.ToIDispatch()
rt := functype.Out(0)
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}
wrapper := reflect.New(rt)
createOleWrapper(rdisp, wrapper.Interface())
val := wrapper.Elem()
if functype.Out(0).Kind() == reflect.Ptr {
val = wrapper
}
results = append(results, val)
} else {
results = append(results, reflect.ValueOf(v.Value()))
}
}
// If the function in the template struct returns at least one value, and the *last* value is
// is an error type, assume that is where error should go. This supports function types
// that return just error, and functions that return (T, error) where T is some other non-error type
//
// Otherwise, if the template function has no return value, and an error occurred, panic with the error
if functype.NumOut() > 0 && isErrorType(functype.Out(functype.NumOut()-1)) {
results = append(results, out[1])
} else {
if err != nil {
panic(err)
}
}
return results
}
}
func isErrorType(t reflect.Type) bool {
errtype := reflect.TypeOf((*error)(nil)).Elem()
return t.Implements(errtype)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment