Last active
December 18, 2020 01:02
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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