diff --git a/com.d b/com.d new file mode 100644 index 0000000..18e5145 --- /dev/null +++ b/com.d @@ -0,0 +1,1189 @@ +/++ + Code for COM interop on Windows. You can use it to consume + COM objects (including several objects from .net assemblies) + and to create COM servers with a natural D interface. + + This code is not well tested, don't rely on it yet. But even + in its incomplete state it might help in some cases. + + ```c# + namespace Cool { + public class Test { + + static void Main() { + System.Console.WriteLine("hello!"); + } + + public int test() { return 4; } + public int test2(int a) { return 10 + a; } + public string hi(string s) { return "hello, " + s; } + } + } + ``` + + Compile it into a library like normal, then `regasm` it to register the + assembly... then the following D code will work: + + --- + import arsd.com; + + interface CsharpTest { + int test(); + int test2(int a); + string hi(string s); + } + + void main() { + auto obj = createComObject!CsharpTest("Cool.Test"); // early-bind dynamic version + //auto obj = createComObject("Cool.Test"); // late-bind dynamic version + + import std.stdio; + writeln(obj.test()); // early-bind already knows the signature + writeln(obj.test2(12)); + writeln(obj.hi("D")); + //writeln(obj.test!int()); // late-bind needs help + //writeln(obj.opDispatch!("test", int)()); + } + --- ++/ +module arsd.com; + +/+ + see: program\comtest.d on the laptop. + + as administrator: from program\cs + c:\Windows\Microsoft.NEt\Framework64\v4.0.30319\regasm.exe /regfile /codebase test.dll + + sn -k key.snk + program\cs\makefile + + test.js in there shows it form wsh too + + i can make it work through IDispatch easily enough, though + ideally you'd have a real interface, that requires cooperation + that the idispatch doesn't thanks to .net doing it for us. + + passing other objects should work too btw thanks to idispatch + in the variants... not sure about arrays tho + + and then fully dynamic can be done with opDispatch for teh lulz. ++/ + +import core.sys.windows.windows; +import core.sys.windows.com; +import core.sys.windows.oaidl; + +import core.stdc.string; +import core.atomic; + +pragma(lib, "advapi32"); +pragma(lib, "uuid"); +pragma(lib, "ole32"); +pragma(lib, "oleaut32"); +pragma(lib, "user32"); + +/* Attributes that help with automation */ + +/// +static immutable struct ComGuid { + /// + this(GUID g) { this.guid = g; } + /// + this(string g) { guid = stringToGuid(g); } + GUID guid; +} + +GUID stringToGuid(string g) { + return GUID.init; // FIXME +} + +bool hasGuidAttribute(T)() { + bool has = false; + foreach(attr; __traits(getAttributes, T)) + static if(is(typeof(attr) == ComGuid)) + has = true; + return has; +} + +template getGuidAttribute(T) { + static ComGuid helper() { + foreach(attr; __traits(getAttributes, T)) + static if(is(typeof(attr) == ComGuid)) + return attr; + assert(0); + } + __gshared static immutable getGuidAttribute = helper(); +} + + +/* COM CLIENT CODE */ + +__gshared int coInitializeCalled; +static ~this() { + CoFreeUnusedLibraries(); + if(coInitializeCalled) { + CoUninitialize(); + coInitializeCalled--; + } +} + +/// +void initializeClassicCom() { + if(coInitializeCalled) + return; + + ComCheck(CoInitialize(null), "COM initialization failed"); + + coInitializeCalled++; +} + +/// +bool ComCheck(HRESULT hr, string desc) { + if(FAILED(hr)) + throw new ComException(hr, desc); + return true; +} + +/// +class ComException : Exception { + this(HRESULT hr, string desc, string file = __FILE__, size_t line = __LINE__) { + this.hr = hr; + import std.format; + super(desc ~ format(" %08x", hr), file, line); + } + + HRESULT hr; +} + +template Dify(T) { + static if(is(T : IUnknown)) { + // FIXME + static assert(0); + } else { + alias Dify = T; + } +} + +import std.traits; + +/// +struct ComClient(DVersion, ComVersion) { + ComVersion innerComObject_; + this(ComVersion t) { + this.innerComObject_ = t; + } + this(this) { + innerComObject_.AddRef(); + } + ~this() { + innerComObject_.Release(); + } + + // note that COM doesn't really support overloading so this + // don't even attempt it. C# will export as name_N where N + // is the index of the overload (except for 1) but... + + static if(is(DVersion == Dynamic)) + template opDispatch(string name) { + template opDispatch(Ret = void) { + Ret opDispatch(Args...)(Args args) { + return dispatchMethodImpl!(name, Ret)(args); + } + } + } + + static if(is(ComVersion == IDispatch)) + template dispatchMethodImpl(string memberName, Ret = void) { + Ret dispatchMethodImpl(Args...)(Args args) { + static if(is(ComVersion == IDispatch)) { + + // FIXME: this can be cached and reused, even done ahead of time + DISPID dispid; + + import std.conv; + wchar*[1] names = [(to!wstring(memberName) ~ "\0"w).dup.ptr]; + ComCheck(innerComObject_.GetIDsOfNames(&GUID_NULL, names.ptr, 1, LOCALE_SYSTEM_DEFAULT, &dispid), "Look up name"); + + DISPPARAMS disp_params; + + static if(args.length) { + VARIANT[args.length] vargs; + foreach(idx, arg; args) { + vargs[idx] = toComVariant(arg); + } + + disp_params.rgvarg = vargs.ptr; + disp_params.cArgs = cast(int) args.length; + } + + VARIANT result; + + ComCheck(innerComObject_.Invoke( + dispid, + &GUID_NULL, LOCALE_SYSTEM_DEFAULT, // whatever + DISPATCH_METHOD, + &disp_params, + &result, + null, // exception info + null // arg error + ), "Invoke"); + + return getFromVariant!(typeof(return))(result); + } else { + static assert(0); // FIXME + } + + } + } + + // so note that if I were to just make this a class, it'd inherit + // attributes from the D interface... but I want the RAII struct... + // could do a class with a wrapper and alias this though. but meh. + static foreach(memberName; __traits(allMembers, DVersion)) { + static foreach(idx, overload; __traits(getOverloads, DVersion, memberName)) { + mixin(q{ReturnType!overload }~memberName~q{(Parameters!overload args) { + return dispatchMethodImpl!(memberName, typeof(return))(args); + } + }); + } + } +} + +VARIANT toComVariant(T)(T arg) { + VARIANT ret; + static if(is(T : int)) { + ret.vt = 3; + ret.intVal = arg; + } else static if(is(T == string)) { + ret.vt = 8; + import std.utf; + ret.bstrVal = SysAllocString(toUTFz!(wchar*)(arg)); + } else static assert(0, "Unsupported type (yet) " ~ T.stringof); + + return ret; +} + +/* + If you want to do self-registration: + + if(dll_regserver("filename.dll", 1) == 0) { + scope(exit) + dll_regserver("filename.dll", 0); + // use it + } +*/ + +// note that HKEY_CLASSES_ROOT\pretty name\CLSID has the guid + +// note: https://en.wikipedia.org/wiki/Component_Object_Model#Registration-free_COM + +GUID guidForClassName(wstring c) { + GUID id; + ComCheck(CLSIDFromProgID((c ~ "\0").ptr, &id), "Name lookup failed"); + return id; +} + +interface Dynamic {} + +/++ + Create a COM object. The passed interface should be a child of IUnknown and from core.sys.windows or have a ComGuid UDA, or be something else entirely and you get dynamic binding. + + The string version can take a GUID in the form of {xxxxx-xx-xxxx-xxxxxxxx} or a name it looks up in the registry. + The overload takes a GUID object (e.g. CLSID_XXXX from the Windows headers or one you write in yourself). + + It will return a wrapper to the COM object that conforms to a D translation of the COM interface with automatic refcounting. ++/ +// FIXME: or you can request a fully dynamic version via opDispatch. That will have to be a thing +auto createComObject(T = Dynamic)(wstring c) { + return createComObject!(T)(guidForClassName(c)); +} +/// ditto +auto createComObject(T = Dynamic)(GUID classId) { + initializeClassicCom(); + + static if(is(T : IUnknown) && hasGuidAttribute!T) { + enum useIDispatch = false; + auto iid = getGuidAttribute!(T).guid; + // FIXME the below condition is just woof + } else static if(is(T : IUnknown) && is(typeof(mixin("core.sys.windows.IID_" ~ T.stringof)))) { + enum useIDispatch = false; + auto iid = mixin("core.sys.windows.IID_" ~ T.stringof); + } else { + enum useIDispatch = true; + auto iid = IID_IDispatch; + } + + static if(useIDispatch) { + IDispatch obj; + } else { + static assert(is(T : IUnknown)); + T obj; + } + + ComCheck(CoCreateInstance(&classId, null, CLSCTX_INPROC_SERVER, &iid, cast(void**) &obj), "Failed to create object"); + + return ComClient!(Dify!T, typeof(obj))(obj); +} + + +// FIXME: add one to get by ProgID rather than always guid +// FIXME: add a dynamic com object that uses IDispatch + + +/* COM SERVER CODE */ + +T getFromVariant(T)(VARIANT arg) { + import std.traits; + import std.conv; + static if(is(T == int)) { + if(arg.vt == 3) + return arg.intVal; + } else static if(is(T == string)) { + if(arg.vt == 8) { + auto str = arg.bstrVal; + return to!string(str[0 .. SysStringLen(str)]); + } + } else static if(is(T == IDispatch)) { + if(arg.vt == 9) + return arg.pdispVal; + } + throw new Exception("Type mismatch, needed "~ T.stringof ~"got " ~ to!string(arg.vt)); + assert(0); +} + +/// Mixin to a low-level COM implementation class +mixin template IDispatchImpl() { + override HRESULT GetIDsOfNames( REFIID riid, OLECHAR ** rgszNames, UINT cNames, LCID lcid, DISPID * rgDispId) { + if(cNames == 0) + return DISP_E_UNKNOWNNAME; + + char[256] buffer; + auto want = oleCharsToString(buffer, rgszNames[0]); + foreach(idx, member; __traits(allMembers, typeof(this))) { + if(member == want) { + rgDispId[0] = idx + 1; + return S_OK; + } + } + return DISP_E_UNKNOWNNAME; + } + + override HRESULT GetTypeInfoCount(UINT* i) { *i = 0; return S_OK; } + override HRESULT GetTypeInfo(UINT i, LCID l, LPTYPEINFO* p) { *p = null; return S_OK; } + override HRESULT Invoke(DISPID dispIdMember, REFIID reserved, LCID locale, WORD wFlags, DISPPARAMS* params, VARIANT* result, EXCEPINFO* except, UINT* argErr) { + // wFlags == 1 function call + // wFlags == 2 property getter + // wFlags == 4 property setter + foreach(idx, member; __traits(allMembers, typeof(this))) { + if(idx + 1 == dispIdMember) { + static if(is(typeof(__traits(getMember, this, member)) == function)) + try { + import std.traits; + ParameterTypeTuple!(__traits(getMember, this, member)) args; + alias argsStc = ParameterStorageClassTuple!(__traits(getMember, this, member)); + + static if(argsStc.length >= 1 && argsStc[0] == ParameterStorageClass.out_) { + // the return value is often the first out param + typeof(args[0]) returnedValue; + + if(params !is null) { + assert(params.cNamedArgs == 0); // FIXME + + if(params.cArgs < args.length - 1) + return DISP_E_BADPARAMCOUNT; + + foreach(aidx, arg; args[1 .. $]) + args[1 + aidx] = getFromVariant!(typeof(arg))(params.rgvarg[aidx]); + } + + static if(is(ReturnType!(__traits(getMember, this, member)) == void)) { + __traits(getMember, this, member)(returnedValue, args[1 .. $]); + } else { + auto returned = __traits(getMember, this, member)(returnedValue, args[1 .. $]); + // FIXME: it probably returns HRESULT so we should forward that or something. + } + + if(result !is null) { + static if(argsStc.length >= 1 && argsStc[0] == ParameterStorageClass.out_) { + result.vt = 3; // int + result.intVal = returnedValue; + } + } + } else { + + if(params !is null) { + assert(params.cNamedArgs == 0); // FIXME + if(params.cArgs < args.length) + return DISP_E_BADPARAMCOUNT; + foreach(aidx, arg; args) + args[aidx] = getFromVariant!(typeof(arg))(params.rgvarg[aidx]); + } + + // no return value of note (just HRESULT at most) + static if(is(ReturnType!(__traits(getMember, this, member)) == void)) { + __traits(getMember, this, member)(args); + } else { + auto returned = __traits(getMember, this, member)(args); + // FIXME: it probably returns HRESULT so we should forward that or something. + } + } + + return S_OK; + } catch(Throwable e) { + // FIXME: fill in the exception info + if(except !is null) { + except.wCode = 1; + import std.utf; + except.bstrDescription = SysAllocString(toUTFz!(wchar*)(e.toString())); + except.bstrSource = SysAllocString("amazing"w.ptr); + } + return DISP_E_EXCEPTION; + } + } + } + + return DISP_E_MEMBERNOTFOUND; + } +} + +/// Mixin to a low-level COM implementation class +mixin template ComObjectImpl() { +protected: + IUnknown m_pUnkOuter; // Controlling unknown + PFNDESTROYED m_pfnDestroy; // To call on closure + + /* + * pUnkOuter LPUNKNOWN of a controlling unknown. + * pfnDestroy PFNDESTROYED to call when an object + * is destroyed. + */ + public this(IUnknown pUnkOuter, PFNDESTROYED pfnDestroy) { + m_pUnkOuter = pUnkOuter; + m_pfnDestroy = pfnDestroy; + } + + ~this() { + //MessageBoxA(null, "CHello.~this()", null, MB_OK); + } + + // Note: you can implement your own Init along with this mixin template and your function will automatically override this one + /* + * Performs any intialization of a CHello that's prone to failure + * that we also use internally before exposing the object outside. + * Return Value: + * BOOL true if the function is successful, + * false otherwise. + */ + public BOOL Init() { + //MessageBoxA(null, "CHello.Init()", null, MB_OK); + return true; + } + + + public + override HRESULT QueryInterface(const (IID)*riid, LPVOID *ppv) { + // wchar[200] lol; auto got = StringFromGUID2(riid, lol.ptr, lol.length); import std.conv; + //MessageBoxA(null, toStringz("CHello.QueryInterface(g: "~to!string(lol[0 .. got])~")"), null, MB_OK); + + assert(ppv !is null); + *ppv = null; + + import std.traits; + foreach(iface; InterfacesTuple!(typeof(this))) { + static if(hasGuidAttribute!iface()) { + auto guid = getGuidAttribute!iface; + if(*riid == guid.guid) { + *ppv = cast(void*) cast(iface) this; + break; + } + } else static if(is(iface == IUnknown)) { + if (IID_IUnknown == *riid) { + *ppv = cast(void*) cast(IUnknown) this; + break; + } + } else static if(is(iface == IDispatch)) { + if (IID_IDispatch == *riid) { + *ppv = cast(void*) cast(IDispatch) this; + break; + } + } + } + + if(*ppv !is null) { + AddRef(); + return NOERROR; + } else { + return E_NOINTERFACE; + } + } + + public + extern(Windows) ULONG AddRef() { + import core.atomic; + return atomicOp!"+="(*cast(shared)&count, 1); + } + + public + extern(Windows) ULONG Release() { + import core.atomic; + LONG lRef = atomicOp!"-="(*cast(shared)&count, 1); + if (lRef == 0) { + // free object + + /* + * Tell the housing that an object is going away so it can + * shut down if appropriate. + */ + //MessageBoxA(null, "CHello Destroy()", null, MB_OK); + + if (m_pfnDestroy) + (*m_pfnDestroy)(); + + // delete this; + return 0; + + + // If we delete this object, then the postinvariant called upon + // return from Release() will fail. + // Just let the GC reap it. + //delete this; + + return 0; + } + + return cast(ULONG)lRef; + } + + LONG count = 0; // object reference count + +} + + + + +// Type for an object-destroyed callback +alias void function() PFNDESTROYED; + +extern (C) +{ + void rt_init(); + void rt_term(); + void gc_init(); + void gc_term(); +} + + +// This class factory object creates Hello objects. +class ClassFactory(Class) : IClassFactory { + extern (Windows) : + + // IUnknown members + override HRESULT QueryInterface(const (IID)*riid, LPVOID *ppv) { + if (IID_IUnknown == *riid) { + *ppv = cast(void*) cast(IUnknown) this; + } + else if (IID_IClassFactory == *riid) { + *ppv = cast(void*) cast(IClassFactory) this; + } + else { + *ppv = null; + return E_NOINTERFACE; + } + + AddRef(); + return NOERROR; + } + + LONG count = 0; // object reference count + ULONG AddRef() { + return atomicOp!"+="(*cast(shared)&count, 1); + } + + ULONG Release() { + return atomicOp!"-="(*cast(shared)&count, 1); + } + + // IClassFactory members + override HRESULT CreateInstance(IUnknown pUnkOuter, IID*riid, LPVOID *ppvObj) { + HRESULT hr; + + *ppvObj = null; + hr = E_OUTOFMEMORY; + + // Verify that a controlling unknown asks for IUnknown + if (null !is pUnkOuter && IID_IUnknown == *riid) + return CLASS_E_NOAGGREGATION; + + // Create the object passing function to notify on destruction. + auto pObj = new Class(pUnkOuter, &ObjectDestroyed); + + if (!pObj) { + MessageBoxA(null, "null", null, 0); + return hr; + } + + if (pObj.Init()) { + hr = pObj.QueryInterface(riid, ppvObj); + } + + // Kill the object if initial creation or Init failed. + if (FAILED(hr)) + delete pObj; + else + g_cObj++; + + return hr; + } + + HRESULT LockServer(BOOL fLock) { + //MessageBoxA(null, "CHelloClassFactory.LockServer()", null, MB_OK); + + if (fLock) + g_cLock++; + else + g_cLock--; + + return NOERROR; + } +} +__gshared ULONG g_cLock=0; +__gshared ULONG g_cObj =0; + +/* + * ObjectDestroyed + * + * Purpose: + * Function for the Hello object to call when it gets destroyed. + * Since we're in a DLL we only track the number of objects here, + * letting DllCanUnloadNow take care of the rest. + */ + +extern (D) void ObjectDestroyed() +{ + //MessageBoxA(null, "ObjectDestroyed()", null, MB_OK); + g_cObj--; +} + + +char[] oleCharsToString(char[] buffer, OLECHAR* chars) { + auto c = cast(wchar*) chars; + auto orig = c; + + size_t len = 0; + while(*c) { + len++; + c++; + } + + auto c2 = orig[0 .. len]; + int blen; + foreach(ch; c2) { + // FIXME breaks for non-ascii + assert(ch < 127); + buffer[blen] = cast(char) ch; + blen++; + } + + return buffer[0 .. blen]; +} + + +// usage: mixin ComServerMain!(CHello, CLSID_Hello, "Hello", "1.0"); +mixin template ComServerMain(Class, string progId, string ver) { + static assert(hasGuidAttribute!Class, "Add a @ComGuid(GUID()) to your class"); + + __gshared HINSTANCE g_hInst; + + // initializing the runtime can fail on Windows XP when called via regsvr32... + + extern (Windows) + BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved) { + import core.sys.windows.dll; + g_hInst = hInstance; + + switch (ulReason) { + case DLL_PROCESS_ATTACH: + return dll_process_attach(hInstance, true); + break; + case DLL_THREAD_ATTACH: + dll_thread_attach(true, true); + break; + case DLL_PROCESS_DETACH: + dll_process_detach(hInstance, true); + break; + + case DLL_THREAD_DETACH: + return dll_thread_detach(true, true); + break; + + default: + assert(0); + } + + return true; + } + + /* + * DllGetClassObject + * + * Purpose: + * Provides an IClassFactory for a given CLSID that this DLL is + * registered to support. This DLL is placed under the CLSID + * in the registration database as the InProcServer. + * + * Parameters: + * clsID REFCLSID that identifies the class factory + * desired. Since this parameter is passed this + * DLL can handle any number of objects simply + * by returning different class factories here + * for different CLSIDs. + * + * riid REFIID specifying the interface the caller wants + * on the class object, usually IID_ClassFactory. + * + * ppv LPVOID * in which to return the interface + * pointer. + * + * Return Value: + * HRESULT NOERROR on success, otherwise an error code. + */ + pragma(mangle, "DllGetClassObject") + export + extern(Windows) + HRESULT DllGetClassObject(CLSID* rclsid, IID* riid, LPVOID* ppv) { + HRESULT hr; + ClassFactory!Class pObj; + + //MessageBoxA(null, "DllGetClassObject()", null, MB_OK); + + // printf("DllGetClassObject()\n"); + + if (clsid != *rclsid) + return E_FAIL; + + pObj = new ClassFactory!Class(); + + if (!pObj) + return E_OUTOFMEMORY; + + hr = pObj.QueryInterface(riid, ppv); + + if (FAILED(hr)) + delete pObj; + + return hr; + } + + /* + * Answers if the DLL can be freed, that is, if there are no + * references to anything this DLL provides. + * + * Return Value: + * BOOL true if nothing is using us, false otherwise. + */ + pragma(mangle, "DllCanUnloadNow") + extern(Windows) + HRESULT DllCanUnloadNow() { + SCODE sc; + + //MessageBoxA(null, "DllCanUnloadNow()", null, MB_OK); + + // Any locks or objects? + sc = (0 == g_cObj && 0 == g_cLock) ? S_OK : S_FALSE; + return sc; + } + + static immutable clsid = getGuidAttribute!Class.guid; + + /* + * Instructs the server to create its own registry entries + * + * Return Value: + * HRESULT NOERROR if registration successful, error + * otherwise. + */ + pragma(mangle, "DllRegisterServer") + extern(Windows) + HRESULT DllRegisterServer() { + char[128] szID; + char[128] szCLSID; + char[512] szModule; + + // Create some base key strings. + MessageBoxA(null, "DllRegisterServer", null, MB_OK); + auto len = StringFromGUID2(&clsid, cast(LPOLESTR) szID, 128); + unicode2ansi(szID.ptr); + szID[len] = 0; + + //MessageBoxA(null, toStringz("DllRegisterServer("~szID[0 .. len] ~")"), null, MB_OK); + + strcpy(szCLSID.ptr, "CLSID\\"); + strcat(szCLSID.ptr, szID.ptr); + + char[200] partialBuffer; + partialBuffer[0 .. progId.length] = progId[]; + partialBuffer[progId.length] = 0; + auto partial = partialBuffer.ptr; + + char[200] fullBuffer; + fullBuffer[0 .. progId.length] = progId[]; + fullBuffer[progId.length .. progId.length + ver.length] = ver[]; + fullBuffer[progId.length + ver.length] = 0; + auto full = fullBuffer.ptr; + + // Create ProgID keys + SetKeyAndValue(full, null, "Hello Object"); + SetKeyAndValue(full, "CLSID", szID.ptr); + + // Create VersionIndependentProgID keys + SetKeyAndValue(partial, null, "Hello Object"); + SetKeyAndValue(partial, "CurVer", full); + SetKeyAndValue(partial, "CLSID", szID.ptr); + + // Create entries under CLSID + SetKeyAndValue(szCLSID.ptr, null, "Hello Object"); + SetKeyAndValue(szCLSID.ptr, "ProgID", full); + SetKeyAndValue(szCLSID.ptr, "VersionIndependentProgID", partial); + SetKeyAndValue(szCLSID.ptr, "NotInsertable", null); + + GetModuleFileNameA(g_hInst, szModule.ptr, szModule.length); + + SetKeyAndValue(szCLSID.ptr, "InprocServer32", szModule.ptr); + return NOERROR; + } + + /* + * Purpose: + * Instructs the server to remove its own registry entries + * + * Return Value: + * HRESULT NOERROR if registration successful, error + * otherwise. + */ + pragma(mangle, "DllUnregisterServer") + extern(Windows) + HRESULT DllUnregisterServer() { + char[128] szID; + char[128] szCLSID; + char[256] szTemp; + + MessageBoxA(null, "DllUnregisterServer()", null, MB_OK); + + // Create some base key strings. + StringFromGUID2(&clsid, cast(LPOLESTR) szID, 128); + unicode2ansi(szID.ptr); + strcpy(szCLSID.ptr, "CLSID\\"); + strcat(szCLSID.ptr, szID.ptr); + + TmpStr tmp; + tmp.append(progId); + tmp.append("\\CurVer"); + RegDeleteKeyA(HKEY_CLASSES_ROOT, tmp.getPtr()); + tmp.clear(); + tmp.append(progId); + tmp.append("\\CLSID"); + RegDeleteKeyA(HKEY_CLASSES_ROOT, tmp.getPtr()); + tmp.clear(); + tmp.append(progId); + RegDeleteKeyA(HKEY_CLASSES_ROOT, tmp.getPtr()); + + tmp.clear(); + tmp.append(progId); + tmp.append(ver); + tmp.append("\\CLSID"); + RegDeleteKeyA(HKEY_CLASSES_ROOT, tmp.getPtr()); + tmp.clear(); + tmp.append(progId); + tmp.append(ver); + RegDeleteKeyA(HKEY_CLASSES_ROOT, tmp.getPtr()); + + strcpy(szTemp.ptr, szCLSID.ptr); + strcat(szTemp.ptr, "\\"); + strcat(szTemp.ptr, "ProgID"); + RegDeleteKeyA(HKEY_CLASSES_ROOT, szTemp.ptr); + + strcpy(szTemp.ptr, szCLSID.ptr); + strcat(szTemp.ptr, "\\"); + strcat(szTemp.ptr, "VersionIndependentProgID"); + RegDeleteKeyA(HKEY_CLASSES_ROOT, szTemp.ptr); + + strcpy(szTemp.ptr, szCLSID.ptr); + strcat(szTemp.ptr, "\\"); + strcat(szTemp.ptr, "NotInsertable"); + RegDeleteKeyA(HKEY_CLASSES_ROOT, szTemp.ptr); + + strcpy(szTemp.ptr, szCLSID.ptr); + strcat(szTemp.ptr, "\\"); + strcat(szTemp.ptr, "InprocServer32"); + RegDeleteKeyA(HKEY_CLASSES_ROOT, szTemp.ptr); + + RegDeleteKeyA(HKEY_CLASSES_ROOT, szCLSID.ptr); + return NOERROR; + } +} + +/* + * SetKeyAndValue + * + * Purpose: + * Private helper function for DllRegisterServer that creates + * a key, sets a value, and closes that key. + * + * Parameters: + * pszKey LPTSTR to the name of the key + * pszSubkey LPTSTR ro the name of a subkey + * pszValue LPTSTR to the value to store + * + * Return Value: + * BOOL true if successful, false otherwise. + */ +BOOL SetKeyAndValue(LPCSTR pszKey, LPCSTR pszSubkey, LPCSTR pszValue) +{ + HKEY hKey; + char[256] szKey; + BOOL result; + + strcpy(szKey.ptr, pszKey); + + if (pszSubkey) + { + strcat(szKey.ptr, "\\"); + strcat(szKey.ptr, pszSubkey); + } + + result = true; + + if (ERROR_SUCCESS != RegCreateKeyExA(HKEY_CLASSES_ROOT, + szKey.ptr, 0, null, REG_OPTION_NON_VOLATILE, + KEY_ALL_ACCESS, null, &hKey, null)) + result = false; + else + { + if (null != pszValue) + { + if (RegSetValueExA(hKey, null, 0, REG_SZ, cast(BYTE *) pszValue, + cast(uint)((strlen(pszValue) + 1) * char.sizeof)) != ERROR_SUCCESS) + result = false; + } + + if (RegCloseKey(hKey) != ERROR_SUCCESS) + result = false; + } + + if (!result) + MessageBoxA(null, "SetKeyAndValue() failed", null, MB_OK); + + return result; +} + +void unicode2ansi(char *s) +{ + wchar *w; + + for (w = cast(wchar *) s; *w; w++) + *s++ = cast(char)*w; + + *s = 0; +} + +/************************************** + * Register/unregister a DLL server. + * Input: + * flag !=0: register + * ==0: unregister + * Returns: + * 0 success + * !=0 failure + */ + +extern (Windows) alias HRESULT function() pfn_t; + +int dll_regserver(const (char) *dllname, int flag) { + char *fn = flag ? cast(char*) "DllRegisterServer" + : cast(char*) "DllUnregisterServer"; + int result = 1; + pfn_t pfn; + HINSTANCE hMod; + + if (SUCCEEDED(CoInitialize(null))) { + hMod=LoadLibraryA(dllname); + + if (hMod > cast(HINSTANCE) HINSTANCE_ERROR) { + pfn = cast(pfn_t)(GetProcAddress(hMod, fn)); + + if (pfn && SUCCEEDED((*pfn)())) + result = 0; + + CoFreeLibrary(hMod); + CoUninitialize(); + } + } + + return result; +} + +struct TmpStr { + char[256] buffer; + int length; + void clear() { length = 0; } + char* getPtr() { + buffer[length] = 0; + return buffer.ptr; + } + + void append(string s) { + buffer[length .. length + s.length] = s[]; + length += s.length; + } +} + +/+ + Goals: + + * Use RoInitialize if present, OleInitialize or CoInitializeEx if not. + (if RoInitialize is present, webview can use Edge too, otherwise + gonna go single threaded for MSHTML. maybe you can require it via + a version switch) + + or i could say this is simply not compatible with webview but meh. + + * idl2d ready to rock + * RAII objects in use with natural auto-gen wrappers + * Natural implementations type-checking the interface + + so like given + + interface Foo : IUnknown { + HRESULT test(BSTR a, out int b); + } + + you can + + alias EasyCom!Foo Foo; + Foo f = Foo.make; // or whatever + int b = f.test("cool"); // throws if it doesn't return OK + + class MyFoo : ImplementsCom!(Foo) { + int test(string a) { return 5; } + } + + and then you still use it through the interface. + + ImplementsCom takes the interface and translates it into + a regular D interface for type checking. + and then makes a proxy class to forward stuff. unless i can + rig it with abstract methods + + class MyNewThing : IsCom!(MyNewThing) { + // indicates this implementation ought to + // become the interface + } + + (basically in either case it converts the class to a COM + wrapper, then asserts it actually implements the required + interface) + + + + or what if i had a private implementation of the interface + in the base class, auto-generated. then abstract hooks for + the other things. ++/ + +/++ + +module com; + +import com2; + +interface Refcounting { + void AddRef(); + void Release(); +} + +interface Test : Refcounting { + void test(); +} + +interface Test2 : Refcounting { + void test2(); +} + +class Foo : Implements!Test, Implements!Test2 { + override void test() { + import std.stdio; + writeln("amazing"); + } + + void test2() {} + + mixin Refcounts; +} +mixin RegisterComImplementation!(Foo, "some-guid"); + +void main() { + auto foo = new Foo(); + auto c = foo.getComProxy(); + c.test(); + +} + ++/ + +/++ + +module com2; + +/+ + The COM interface's implementation is done by a + generated class, forwarding it to the other D + implementation + + if it implements IDispatch then it can do the dynamic + thing too automatically! ++/ + +template Implements(Interface) { + private static class Helper : Interface { + Implements i; + this(Implements i) { + this.i = i; + } + + static foreach(memberName; __traits(allMembers, Interface)) + mixin(q{ void } ~ memberName ~ q{ () { + import std.stdio; writeln("wrapper begin"); + __traits(getMember, i, memberName)(); + writeln("wrapper end"); + }}); + } + + interface Implements { + final Helper getComProxy() { + return new Helper(this); + } + + static foreach(memberName; __traits(allMembers, Interface)) + mixin(q{ void } ~ memberName ~ q{ (); }); + + mixin template Refcounts() { + int refcount; + void AddRef() { refcount ++; } + void Release() { refcount--; } + } + } +} + +// the guid may also be a UDA on Class, but you do need to register your implementations +mixin template RegisterComImplementation(Class, string guid = null) { + +} + +// wraps the interface with D-friendly type and provides RAII for the object +struct ComClient(I) {} +// eg: alias XmlDocument = ComClient!IXmlDocument; +// then you get it through a com factory + +ComClient!I getCom(T)(string guid) { return ComClient!I(); } + ++/ diff --git a/comhelpers.d b/comhelpers.d index 884212e..bd5caec 100644 --- a/comhelpers.d +++ b/comhelpers.d @@ -3,6 +3,27 @@ +/ module arsd.comhelpers; +/+ + see: program\comtest.d on the laptop. + + as administrator: from program\cs + c:\Windows\Microsoft.NEt\Framework64\v4.0.30319\regasm.exe /regfile /codebase test.dll + + sn -k key.snk + program\cs\makefile + + test.js in there shows it form wsh too + + i can make it work through IDispatch easily enough, though + ideally you'd have a real interface, that requires cooperation + that the idispatch doesn't thanks to .net doing it for us. + + passing other objects should work too btw thanks to idispatch + in the variants... not sure about arrays tho + + and then fully dynamic can be done with opDispatch for teh lulz. ++/ + import core.sys.windows.windows; import core.sys.windows.com; import core.sys.windows.oaidl; @@ -94,7 +115,12 @@ struct AutoComPtr(T) { } */ +// note that HKEY_CLASSES_ROOT\pretty name\CLSID has the guid + /// Create a COM object. the string params are GUID literals that i mixin (this sux i know) +/// or if the interface has no IID it will try to IDispatch it +/// or you can request a fully dynamic version via opDispatch. +/// note i can try `import core.sys.windows.uuid; IID_IDispatch` for example to generically look up ones from the system if they are not attached and come from the windows namespace AutoComPtr!T createObject(T, string iidStr = null)(GUID classId) { initializeCom(); @@ -105,9 +131,10 @@ AutoComPtr!T createObject(T, string iidStr = null)(GUID classId) { T obj; - auto hr = CoCreateInstance(&classId, null, CLSCTX_ALL, &iid, cast(void**) &obj); + auto hr = CoCreateInstance(&classId, null, CLSCTX_INPROC_SERVER, &iid, cast(void**) &obj); + import std.format; if(FAILED(hr)) - throw new Exception("Failed to create object"); + throw new Exception("Failed to create object " ~ format("%08x", hr)); return AutoComPtr!T(obj); } @@ -820,3 +847,102 @@ struct TmpStr { length += s.length; } } + + +/++ + +module com; + +import com2; + +interface Refcounting { + void AddRef(); + void Release(); +} + +interface Test : Refcounting { + void test(); +} + +interface Test2 : Refcounting { + void test2(); +} + +class Foo : Implements!Test, Implements!Test2 { + override void test() { + import std.stdio; + writeln("amazing"); + } + + void test2() {} + + mixin Refcounts; +} +mixin RegisterComImplementation!(Foo, "some-guid"); + +void main() { + auto foo = new Foo(); + auto c = foo.getComProxy(); + c.test(); + +} + ++/ + +/++ + +module com2; + +/+ + The COM interface's implementation is done by a + generated class, forwarding it to the other D + implementation + + if it implements IDispatch then it can do the dynamic + thing too automatically! ++/ + +template Implements(Interface) { + private static class Helper : Interface { + Implements i; + this(Implements i) { + this.i = i; + } + + static foreach(memberName; __traits(allMembers, Interface)) + mixin(q{ void } ~ memberName ~ q{ () { + import std.stdio; writeln("wrapper begin"); + __traits(getMember, i, memberName)(); + writeln("wrapper end"); + }}); + } + + interface Implements { + final Helper getComProxy() { + return new Helper(this); + } + + static foreach(memberName; __traits(allMembers, Interface)) + mixin(q{ void } ~ memberName ~ q{ (); }); + + mixin template Refcounts() { + int refcount; + void AddRef() { refcount ++; } + void Release() { refcount--; } + } + } +} + +// the guid may also be a UDA on Class, but you do need to register your implementations +mixin template RegisterComImplementation(Class, string guid = null) { + +} + +// wraps the interface with D-friendly type and provides RAII for the object +struct ComClient(I) {} +// eg: alias XmlDocument = ComClient!IXmlDocument; +// then you get it through a com factory + +ComClient!I getCom(T)(string guid) { return ComClient!I(); } + ++/ diff --git a/jni.d b/jni.d index e84d838..3b4e07b 100644 --- a/jni.d +++ b/jni.d @@ -191,6 +191,114 @@ private string getJavaName(alias a)() { return name; } +/+ +/+ Java class file definitions { +/ +// see: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.6 + +struct cp_info { + + enum CONSTANT_Class = 7; // sizeof = 2 + enum CONSTANT_Fieldref = 9; // sizeof = 4 + enum CONSTANT_Methodref = 10; // sizeof = 4 + enum CONSTANT_InterfaceMethodref = 11; // sizeof = 4 + enum CONSTANT_String = 8; // sizeof = 2 + enum CONSTANT_Integer = 3; // sizeof = 4 + enum CONSTANT_Float = 4; // sizeof = 4 + enum CONSTANT_Long = 5; // sizeof = 8, but eats two slots + enum CONSTANT_Double = 6; // sizeof = 8, but eats two slots + enum CONSTANT_NameAndType = 12; // sizeof = 2 + enum CONSTANT_Utf8 = 1; // sizeof = 2 + length + enum CONSTANT_MethodHandle = 15; // sizeof = 3 + enum CONSTANT_MethodType = 16; // sizeof = 2; descriptor index + enum CONSTANT_InvokeDynamic = 18; // sizeof = 4 + + ubyte tag; + union { + + } + ubyte[] info; +} + +struct field_info { + + enum ACC_PUBLIC = 0x0001; + enum ACC_PRIVATE = 0x0002; + enum ACC_PROTECTED = 0x0004; + enum ACC_STATIC = 0x0008; + enum ACC_FINAL = 0x0010; + enum ACC_VOLATILE = 0x0040; + enum ACC_TRANSIENT = 0x0080; + enum ACC_SYNTHETIC = 0x1000; + enum ACC_ENUM = 0x4000; + + ushort access_flags; + ushort name_index; + ushort descriptor_index; + ushort attributes_count; + attribute_info attributes[attributes_count]; +} + +struct method_info { + ushort access_flags; + ushort name_index; + ushort descriptor_index; + ushort attributes_count; + attribute_info attributes[attributes_count]; + + enum ACC_PUBLIC = 0x0001; + enum ACC_PRIVATE = 0x0002; + enum ACC_PROTECTED = 0x0004; + enum ACC_STATIC = 0x0008; + enum ACC_FINAL = 0x0010; + enum ACC_SYNCHRONIZED = 0x0020; + enum ACC_BRIDGE = 0x0040; + enum ACC_VARARGS = 0x0080; + enum ACC_NATIVE = 0x0100; + enum ACC_ABSTRACT = 0x0400; + enum ACC_STRICT = 0x0800; + enum ACC_SYNTHETIC = 0x1000; +} + +struct attribute_info { + ushort attribute_name_index; + uint attribute_length; + ubyte[attribute_length] info; +} + +struct ClassFile { + + + enum ACC_PUBLIC = 0x0001; + enum ACC_FINAL = 0x0010; + enum ACC_SUPER = 0x0020; + enum ACC_INTERFACE = 0x0200; + enum ACC_ABSTRACT = 0x0400; + enum ACC_SYNTHETIC = 0x1000; + enum ACC_ANNOTATION = 0x2000; + enum ACC_ENUM = 0x4000; + + + uint magic; + ushort minor_version; + ushort major_version; + ushort constant_pool_count; + cp_info constant_pool[constant_pool_count-1]; + ushort access_flags; + ushort this_class; + ushort super_class; + ushort interfaces_count; + ushort interfaces[interfaces_count]; + ushort fields_count; + field_info fields[fields_count]; + ushort methods_count; + method_info methods[methods_count]; + ushort attributes_count; + attribute_info attributes[attributes_count]; +} + +/+ } end java class file definitions +/ ++/ + // semi-FIXME: java.lang.CharSequence is the interface for String. We should support that just as well. // possibly other boxed types too, like Integer. // FIXME: in general, handle substituting subclasses for interfaces nicely @@ -201,11 +309,14 @@ private string getJavaName(alias a)() { // FIXME: what about the parent class of the java object? Best we can probably do is an interface but perhaps it could be auto-generated by the JavaClass magic. It could take the list and just copy the @Import items. +// FIXME: interfaces? I think a Java interface should just generally be turned into a D interface, but also including the IJavaObject. Basically just write D. No @Import or @Export on this level. +// Just needs a package name somewhere.... +// +// Then the D compiler forces you to declare an implementation of it, and that can be @Import. + /+ - lol if i wanted to try defining a new class in D, i'd prolly have to ctfe generate bytecode trampoline - to make a Java method that forwards to the native call. the actual method names would then need a magical - prefix or something to avoid namespace conflicts between the Java and the D. - I don't think I'll actually do it. But I legit think that is possible. + FIXME lol if i wanted to try defining a new class in D..... you don't even need a trampoline method. Java and native methods can override each other!!! + Perhaps could be like final class AllNew : JavaClass("package", AllNew, true) { @Virtual void foo() {} // defines it here, but Java can override @@ -214,7 +325,8 @@ private string getJavaName(alias a)() { and then @Import and @Export continues to work the same way. +/ -// speaking of hacking bytecode we could prolly read signatures out of a .class file too. +// FIXME: speaking of hacking bytecode we could prolly read signatures out of a .class file too. +// and generate D classes :P // see: https://developer.android.com/training/articles/perf-jni.html @@ -600,6 +712,8 @@ private mixin template JavaImportImpl(T, alias method, size_t overloadIndex) { auto jobj = this_.getJavaHandle(); if(!_jmethodID) { auto jc = (*env).GetObjectClass(env, jobj); + // just a note: jc is an instance of java.lang.Class + // and we could call getName on it to fetch a String to ID it _jmethodID = (*env).GetMethodID(env, jc, getJavaName!method.ptr, // java method string is (args)ret @@ -895,9 +1009,7 @@ private mixin template JavaExportImpl(T, alias method, size_t overloadIndex) { +/ extern(System) - //pragma(mangle, JniMangle()) - // I need it in the DLL, but want it to be not accessible from outside... alas. - export /*private*/ static DTypesToJni!(ReturnType!method) privateJniImplementation(JNIEnv* env, jobject obj, DTypesToJni!(Parameters!method) args) { + private static DTypesToJni!(ReturnType!method) privateJniImplementation(JNIEnv* env, jobject obj, DTypesToJni!(Parameters!method) args) { // set it up in the thread for future calls ActivateJniEnv thing = ActivateJniEnv(env);