rodrigocfd/windigo

[Feature] DDE support/bindings

Opened this issue · 13 comments

DDE (dynamic data exchange, DDE docs) is used widely in Windows and it'd be great if there were a way to create (at least) DDE clients and (possibly) DDE servers in Go using windigo.

Oh yes, I remember using DDE a long time ago. Are these the APIs you need?

Oh yes, I remember using DDE a long time ago. Are these the APIs you need?

Sorry, I should've been more specific. These are APIs I'm looking to use in my project (if possible), the ddeml.h header.
The outside repo seems to be the only repo I have found to have some kind of implementation of this in Go but it hasn't been updated for years and uses cgo.

I've committed the first calls. I believe the API is clean enough:

hdde, err := win.DdeInitialize(
	func(wType co.XTYP, wFmt uint32, hConv win.HCONV,
		hsz1, hsz2 win.HSZ, hData, dwData1, dwData2 uintptr) uintptr {
		return 0
	},
	co.AFCMD_APPCLASS_STANDARD,
)
if err != nil {
	panic(err)
}
defer hdde.DdeUninitialize()

ss := hdde.DdeCreateStringHandle("Hello")
defer hdde.DdeFreeStringHandle(ss)
println(hdde.DdeQueryString(ss))

println("DONE")

Please let me know if they work for you.

Thank you for working on this btw. That code you gave works as expected. However I am mainly interested in the DdeClientTransaction and DdeConnect functions, so I will really appreciate it if you could potentially implement those - I want to be able to create DDE servers and also make exchanges to DDE servers as a client.

I just commited the DdeClientTransaction and DdeFreeDataHandle, but I really have no idea how to test them.

Can you check if are they OK?

This Python code creates a DDE server named py_dde_server and it has one topic my_topic which has one item my_item, that is the current time.

import time
import win32ui, dde
from pywin.mfc import object


class DDETopic(object.Object):
    def __init__(self, topicName):
        self.topic = dde.CreateTopic(topicName)
        object.Object.__init__(self, self.topic)
        self.items = {}

    def setData(self, itemName, value):
        try:
            self.items[itemName].SetData( str(value) )
        except KeyError:
            if itemName not in self.items:
                self.items[itemName] = dde.CreateStringItem(itemName)
                self.topic.AddItem( self.items[itemName] )
                self.items[itemName].SetData( str(value) )


server = dde.CreateServer()
server.Create("py_dde_sever")
ddeTopic = DDETopic("my_topic")
server.AddTopic(ddeTopic)

while True:
    current_time = time.time() # is a number like, 1656394098.8112526
    ddeTopic.setData("my_item", current_time)
    win32ui.PumpWaitingMessages(0, -1)
    time.sleep(0.5)

Attempting to query the server with this Go code,

package main

import (
	"log"

	"github.com/rodrigocfd/windigo/win"
	"github.com/rodrigocfd/windigo/win/co"
)

func main() {
	hdde, err := win.DdeInitialize(
		func(wType co.XTYP, wFmt uint32, hConv win.HCONV,
			hsz1, hsz2 win.HSZ, hData, dwData1, dwData2 uintptr) uintptr {
			return 0
		},
		co.AFCMD_APPCLASS_STANDARD,
	)

	if err != nil {
		panic(err)
	}

	defer hdde.DdeUninitialize()

	name := win.StrOptSome("py_dde_server")
	topic := win.StrOptSome("my_topic")
	item := win.StrOptSome("my_item")

	// *Error is returned*
	hconv, err := hdde.DdeConnect(name, topic, nil)

	defer hdde.DdeDisconnect(hconv)

	if err != nil {
		panic(err)
	}

	var data []byte

	// 0x1030 = XTYP_ADVSTART
	k, err := hdde.DdeClientTransaction(data, hconv, item, 0, 0x1030, 10)
	if err != nil {
		panic(err)
	}

	log.Println(k)
	log.Println(data)
}

Results in an error;

panic: [16394 0x400a]

goroutine 1 [running]:
main.main()
        C:/Users/Jaden/Desktop/dde/cmd/main.go:34 +0x225
exit status 2

Hopefully it is easier to test with the python -- since it is like the only programming language other than C(++) that has it in its lib. DDE feels so legacy 😐.

As far as I could grasp from the documentation, when calling DdeConnect, you must pass the name of an existing service to connect to. Or, don't pass any name at all to connect to any available server.

So, the code below connects without errors:

func main() {
	hDde, err := win.DdeInitialize(func(
		wType co.XTYP, wFmt uint32, hConv win.HCONV,
		hsz1, hsz2 win.HSZ, hData, dwData1, dwData2 uintptr) uintptr {
		return 0
	}, co.AFCMD_APPCLASS_STANDARD)
	if err != nil {
		panic(err)
	}
	defer hDde.DdeUninitialize()

	hConv, err := hDde.DdeConnect(win.StrOptNone(), win.StrOptNone(), nil)
	if err != nil {
		panic(err)
	}
	defer hDde.DdeDisconnect(hConv)

	hData, err := hDde.DdeClientTransaction(
		nil, hConv, win.StrOptNone(), co.CF(0), co.XTYP_ADVSTART, 10)
	if err != nil {
		panic(err)
	}
	defer hDde.DdeFreeDataHandle(hData)
}

However, DdeClientTransaction is returning INVALIDPARAMETER, so now it seems it's just a matter of passing the correct parameters.

Please let me know if it's working now.

In the Python example of @jaevor, you are creating a DDE server, and none of the APIs requested by @AfricanElephant do that, as far as I could understand. We need the correct API to this.

So, my aim is to query DDE services via Go.
This python script here serves as a service to test on:

import time
import win32ui, dde
from pywin.mfc import object

class DDETopic(object.Object):
    ...

server = dde.CreateServer()
server.Create("py_dde_server")
dde_topic = DDETopic("my_topic")
server.AddTopic(dde_topic)

while True:
    current_time = time.time()
    dde_topic.setData("my_item", current_time)
    win32ui.PumpWaitingMessages(0, -1)
    time.sleep(1)

Attempting a DdeClientTransaction like this, panics with a NOTPROCESSED error.

// ...

name := win.StrOptSome("py_dde_server")
topic := win.StrOptSome("my_topic")
item := win.StrOptSome("my_item")

hConv, err := hDde.DdeConnect(name, topic, nil)

if err != nil {
panic(err)
}
defer hDde.DdeDisconnect(hConv)

hData, err := hDde.DdeClientTransaction(nil, hConv, item, co.CF(0), co.XTYP_ADVSTART, 10_000)
if err != nil {
  panic(err)
}
defer hDde.DdeFreeDataHandle(hData)

log.Println(hData)

The python service is functional, as it replies if I query it with a CLI tool or from Excel.
The arguments passed in this ^ code seem fine, however I don't understand the clipboard stuff (co.CF(0)) so I'm not sure if that could possibly not be set right, or if it's something else. What do you think?

@AfricanElephant What's the Python code you're using to query the DDE service?

This is the full code. From https://stackoverflow.com/a/33282523

import time
import win32ui, dde
from pywin.mfc import object

class DDETopic(object.Object):
    def __init__(self, topicName):
        self.topic = dde.CreateTopic(topicName)
        object.Object.__init__(self, self.topic)
        self.items = {}

    def setData(self, itemName, value):
        try:
            self.items[itemName].SetData(str(value))
        except KeyError:
            if itemName not in self.items:
                self.items[itemName] = dde.CreateStringItem(itemName)
                self.topic.AddItem(self.items[itemName])
                self.items[itemName].SetData(str(value))

server = dde.CreateServer()
server.Create("py_dde_server")
dde_topic = DDETopic("my_topic")
server.AddTopic(dde_topic)

while True:
    current_time = time.time()
    dde_topic.setData("my_item", current_time)
    win32ui.PumpWaitingMessages(0, -1)
    time.sleep(1)

Where can I find the source code of this Python library? This library is completely different from all native APIs, its author created a whole different abstraction over it. I'll have to dig into the source code to see what APIs are being actually called.

It seems to be a built-in Python library. For me it lives at ...Python310/Lib/site-packages/pythonwin/dde.pyd.

.pyd files are like Windows DLL files (they're unreadable) so I imagine it's possible to find the specifics in the (C)Python source although I've had no luck. Strange that they've made their own kind of abstraction.

I still don't know exactly what you need, but I found this C++ example, which looks pretty close.

All the needed functions are implemented, is this what you need?