Windows-specific C/C++: DLL access works, but...

Monkey Targets Forums/Desktop/Windows-specific C/C++: DLL access works, but...

DruggedBunny(Posted 2014) [#1]
Hi all,

Wondering if anyone can help with this! I've wanted to access DLLs on Windows for a while, which requires C++ wrapper code, so I finally pieced together this example.

Please bear in mind that my C skills are minimal and my C++ skills non-existent, and take a look at this Monkey test and associated C++ code which would perhaps best be described as "execrable":

test.monkey:
Import "test.cpp"

Extern

Function InitDLL:Int ()
Function FreeDLL:Void ()

Function TestFunc:Int () ' A test Win32 function, no DLL call involved

' Attempt to call MessageBox (HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType)
' from user32.dll:

Function MsgBox (hWnd:Int, lpText:String, lpCaption:String, uType:Int)

' OK, so I knew String wouldn't work, but how do I pass a pointer to a character array?!

Public

Function Main ()

	If InitDLL ()								' Works!
	
		' MsgBox (0, "Hello", "Hello you", 0)	' No worky! Uncomment to try...
		
		'Print TestFunc ()						' Works! Uncomment for requester...
		FreeDLL ()								' Works!
	
	Else
		Print "Failed to open DLL!"
	Endif
	
End


Using skid's Steam example code, and hours of internet research to remember how to do even the simplest of C code, I managed to slowly put together something that seems to work, though I'm not really sure how all the C++ versus extern "C" stuff works...

test.cpp:

// Think these do something... faster compiles... probably.

#define VC_EXTRALEAN
#define WIN32_LEAN_AND_MEAN

#define BBDECL extern "C" // Stolen from skid's Steam code

#include <windows.h>
#include <iostream>

// Just for cout << "Hello" stuff...

using namespace std;

// Global DLL handle...

HINSTANCE User32;

// Typedef thingy for User32's MessageBox...

BBDECL typedef int (*LPMSGBOX)(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);

// MessageBox function pointer... thing...

LPMSGBOX MsgBox;

// Prototypes...

BBDECL int InitDLL ();	// Open DLL and stick in global User32
BBDECL void FreeDLL ();	// Close DLL

// Functions...

BBDECL int InitDLL ()
{

	User32 = LoadLibrary ("user32.dll");
	
	if (User32 != NULL)
	{
		// Point MsgBox to User32's MessageBox function...
		
		MsgBox = (LPMSGBOX)GetProcAddress ((HMODULE)User32, "MessageBox");
		return 1;
	}
	else
	{
		return 0;
	}
	
}

BBDECL void FreeDLL ()
{
	if (User32 != NULL)
	{
		FreeLibrary (User32);
	}
}

// Non-DLL test!

BBDECL int TestFunc ()
{
	MessageBox( NULL, "Hello World!", "Hello", MB_OK );
	return 1;
}


Anyway, that's all working, until I get to the MessageBox function, which I'm trying to dynamically obtain from user32.dll.

MessageBox is defined as MessageBox (HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType), and where it all falls down is when I try to use it from the Monkey side.

Because we can't use byte pointers, as I'd do in BlitzMax (or even a Blitz3D userlib), I don't know how to handle the function definition in Monkey in order to handle the LPCTSTRs, lpText and lpCaption:

Function MsgBox (hWnd:Int, lpText:String, lpCaption:String, uType:Int)


Well, I knew that wouldn't work (still tried it!), but how would I pass a string to the Win32 function requiring a C string pointer? In BlitzMax it'd be easy -- "Hello world".ToCString () -- but I'm lost here!

If anyone can point me in the right direction, or even just comment on how this might be better laid out overall (or where I might be doing something stupid on the C++ side), it would be much appreciated!


AdamRedwoods(Posted 2014) [#2]
in monkey/native/lang.cpp there is a string.ToCString() function in the String Class. (and ToNSString )
so you would need an inbetween function (glue code) to convert the cstring.


DruggedBunny(Posted 2014) [#3]
Thanks for looking into it, Adam... suspect this might be a little out of my C/C++ league (nice to discover there is at least a ToCString)... will have another look tomorrow, though...


DruggedBunny(Posted 2014) [#4]
Nope, as I suspected, I haven't got a clue what to do with this!

I can see the ToCString thing (well, two of them... can't decipher what they mean!) and presumably I need to be writing a C++ function to 'receive' a Monkey String and then call the Win32 function with the output, which sounds do-able, but how do I actually access String/ToCString from an arbitrary C++ file?

I tried a straight #include but of course it's all duplicate identifiers!


Pharmhaus(Posted 2014) [#5]
Using Strings is a no brainer, but the LPCTSTR requires additional conversion.
Unfortunately there is no such function in Monkey and It complains when I try to pass other stuff to it...
This works but is a bit tedious





DruggedBunny(Posted 2014) [#6]
Ah, thanks for that! I can work with this though it does seem a little clumsy as you suggest.

Is this using Monkey's own ToCString? [EDIT: Yes, it is.] Didn't realise I could just use it like that...

Hopefully this will be enough for me to do what I need, thanks both!


DruggedBunny(Posted 2014) [#7]
Hmm, slight problem: if I try to call the DLL version, MsgBox, inside MessageBoxWrapper (instead of the static-linked MessageBox), then it crashes with a memory violation.

(MsgBox is obtained in InitDLL.)

BBDECL void MessageBoxWrapper(HWND hWnd, String lpText, String lpCaption, UINT uType)
{
	MsgBox( NULL, lpText.ToCString<char>(), lpCaption.ToCString<char>(), MB_OK );
}

' Also tried:

BBDECL void MessageBoxWrapper(HWND hWnd, String lpText, String lpCaption, UINT uType)
{
	MsgBox( NULL, (LPCTSTR)lpText.ToCString<char>(), (LPCTSTR)lpCaption.ToCString<char>(), MB_OK );
}

' (Also tried changing the return type to int.)



Any ideas?! As you can see, my grasp of C/C++ leaves a lot to be desired...


Danilo(Posted 2014) [#8]
MessageBox:
Unicode and ANSI names: MessageBoxW (Unicode) and MessageBoxA (ANSI)

And check the return value of GetProcAddress(). The 'A' and 'W' thing is for most functions that work with char/wchar strings.


Pharmhaus(Posted 2014) [#9]
Oh, sorry my fault.
The Loading of the MessageBox function can never succeed becaused it is not called "MessageBox".
Replace "MessageBox" with "MesaageBoxA" because there are two versions of MessageBox (MessageBoxA and MessageBoxW).
And it works again!
MessageBox is just for lazy programmers and is one of the two underneath the hood, depending if you use unicode or not.


Pharmhaus(Posted 2014) [#10]
I Just tested this which might me more of your interest.
I takes the messagebox stuff back to the Monkey-X Level (yay!).
It 'tricks' trans into believing that the MsgBox will need a String.
You only need to be careful what you pass to MsgBox because it still needs a CString.

EDIT: Just continued to hack around and seems more solid now. It now also abuses the type comparison of trans which prevents compilation errors

EDIT2: Made some minor changes to make it even safer.

EDIT3: resolved crash in release mode

How does this work?

Works pretty well so far:





DruggedBunny(Posted 2014) [#11]
Thanks Pharmhaus! That does seem to work, though I found that it crashes in release mode after showing the messagebox, yet debug mode flags up no errors at all, so something's not quite right.

I also amended your first version to use MessageBoxA (I was aware of the A/W thing but apparently didn't even think of it) but that does exactly the same thing -- shows the messagebox but then crashes in release mode. I'll have to stick a few Prints in there and see if I can narrow it down -- the only code after that on the Monkey side is FreeDLL, but removing that shows it's not the cause.

(Also, good point, Danilo -- I did intend to get around to checking the GetProcAddress result, but... ahem!)


DruggedBunny(Posted 2014) [#12]
This is weird... the messagebox displays, but it seems to be crashing only on exit, as Prints/couts anywhere on the Monkey/C++ side all get displayed. It's also nothing to do with the ToCString stuff, as this also crashes:

MsgBox( NULL, "Hello", "Hello again", MB_OK );


I'm testing with the 'old' wrapper version, just because I understand that -- I've no idea how the NativeCString version works, though I'll probably switch to it once the crashes are sorted out!



test.cpp...




Pharmhaus(Posted 2014) [#13]
oh, I was using debug all the time :(
I will try to sort it out but I have to do some chores now.


DruggedBunny(Posted 2014) [#14]
Any help would be much appreciated, but there's no rush!

I did just hack in Kernel32's Sleep function in order to test that it wasn't crashing straight after the MessageBox call -- sure enough, the MB appears, and after closing it, the program sleeps for two seconds, then it crashes.

It only crashes if it calls MsgBox inside MessageBoxWrapper (regardless of whether or not you use ToCString), so that's definitely related, but the definitions do all appear to be correct, and as far as I can tell it's only on program exit.

Hmm, interesting... this crashed after the second messagebox appeared, so it's not necessarily at program exit (Delay is just Kernel32/Sleep):

BBDECL int MessageBoxWrapper(HWND hWnd, String lpText, String lpCaption, UINT uType)
{
	MsgBox( NULL, lpText.ToCString<char>(), lpCaption.ToCString<char>(), MB_OK );
	Delay(2000);
	MsgBox( NULL, lpText.ToCString<char>(), lpCaption.ToCString<char>(), MB_OK );
	Delay(2000);
	MsgBox( NULL, lpText.ToCString<char>(), lpCaption.ToCString<char>(), MB_OK );
	Delay(2000);
//	MsgBox( NULL, "Hello", "Hello you", MB_OK );
}



Hmm #2: removing the Delays, it sometimes gets through all before crashing...


Pharmhaus(Posted 2014) [#15]
I'm not sure if it works because everything is ok or if it is some kind of random-ness.
Try to replace your version with these lines:
// Typedef thingy for User32's MessageBox...

BBDECL typedef int (__stdcall *LPMSGBOX)(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);




DruggedBunny(Posted 2014) [#16]
Ah, perfect! I believe that's what was missing -- it's the equivalent of BlitzMax's "win32" calling type. Should have guessed, but I wouldn't have known how to actually apply it even if I did!

Thanks for all your help with this -- I've learned a lot just from this little exercise! Think I have enough to work with now.


Danilo(Posted 2014) [#17]
You may want to use UNICODE strings with WinAPI (MessageBoxW for example).
Monkey X defines the type "Char" at the beginning of C++ files, so if you use ToCString<Char>()
it uses Unicode.

MessageBox.cpp
#include <windows.h>

namespace WinAPI {
    inline int MessageBox(int a,String b,String c, int d) {
        return MessageBoxW((HWND)a,b.ToCString<Char>(),c.ToCString<Char>(),d);
    }
}

#define MsgBox(a,b,c,d)      MessageBoxW((HWND)a,b.ToCString<Char>(),c.ToCString<Char>(),d)
#define MessageBox_(a,b,c,d) MessageBoxW((HWND)a,b.ToCString<Char>(),c.ToCString<Char>(),d)

MessageBox.monkey
Import "MessageBox.cpp"

Extern
    Function MsgBox:Int(hWnd:Int, lpText:String, lpCaption:String, uType:Int)
    Function MessageBox_:Int(hWnd:Int, lpText:String, lpCaption:String, uType:Int)
    Class WinAPI Abstract
        Function MessageBox:Int(hWnd:Int, lpText:String, lpCaption:String, uType:Int)
    End
Public

Function Main:Int()
    MsgBox(0, "Helloöäü - مرحبا", "Hello you", 0)
    MessageBox_(0, "Helloöäü - مرحبا", "Hello you", 0)
    WinAPI.MessageBox(0, "Helloöäü - مرحبا", "Hello you", 0)
End



DruggedBunny(Posted 2014) [#18]
Thanks, Danilo, you're right, though I was mainly using MessageBox/user32.dll as a test for accessing 3rd-party DLLs.

However, that's an interesting approach you've taken there, so will study it -- and of course it is much better to be able to support other languages anyway!


DruggedBunny(Posted 2014) [#19]
@Pharmhaus: Following on from the Blitz Hardwired thread, here's an initial version of the Monkey lib:

http://www.hi-toro.com/monkey/wiredmonkey.zip

I used your NativeCString hack (I "sort of" understand it from your explanation and it was the easiest way to handle it!), but for some reason my attempt to load a texture fails, and I assume it's related to the string:

texture	= dxLoadTexture (NativeCString ("boing.jpg"), 1)


Would you mind taking a look to see where I'm going wrong? (The imports are based on Ploppy's latest release version.)

I'm wondering if it's an ASCII versus Unicode thing (Blitz3D was ASCII, and I'd assume Hardwired is the same given it's really made for Blitz3D)... ?

Also, not sure how to handle returned strings with this -- any ideas there?

Hopefully you can just tweak the included CPP/Monkey import files to test!


Pharmhaus(Posted 2014) [#20]

I was actually a little uncomfortable using that method, though, mainly because I don't understand how it works!


The problem in Monkey is that there is no CString like there was in Blitzmax (we agree on that).
So the magic questions that pop up are :
what kind of parameter does a function have if there is no cstring, but the function *needs* a cstring to work with?
Furthermore, what return type does a function have which likes to return a CString? (which does not exist in Monkey)

We use the Extern Keyword there.
With extern we tell Trans (the Monkey compiler) that there is a Function or Class somewhere outside of our Monkey files that we would like to use.
The magic trick: trans can't tell if we are a liar or not.
So you could write what ever you wanted inside extern and trans is going to believe that no matter how absurd, because It trusts you.
So we lie to trans :
This function returns an object of the Class CStringNative
'Converts String to CString, please note that there is no valid datatype in monkey so we use the CStringNative hack
Function NativeCString:CStringNative(S:String)

Class CStringNative Abstract
End

and trans will believe that.
This is a bit dangerous because we could trap ourself somewhere because of our lies.
e.g. we cannot create a new CStringNative, because there is no such class but a user could type "new CStringNative".
I fixed that by making Trans also believe that CStringNative is abstract (can't be build via new() ), so we cannot store the return type in a variable which would be deadly to ourself.
To make it work we also make trans believe that Messagebox needs an object of type CStringNative to work with.
Function MsgBox(hWnd:Int, lpText:CStringNative, lpCaption:CStringNative, uType:Int)

You could replace the Messagebox code with something really absurd
e.g. lpText is of type int and the function NativeCString has int as return type.

This is still going to work because it doesn't change the code generated.
This is the major trick here: things in extern don't generate any code at all (because they already 'exist').
Trans will glue all the code together no matter if it makes sense or not.
What counts is the world from trans point of view and in trans world everything is fine (because int=int; CStringNative=CStringNative).

The reason the CStringNative Class approach is way safer is because trans will do type-checking now.
if we made lpText an 'int' we could pass normal ints to it because from trans view, this is fine.
By saying 'this needs to be a CStringNative' trans will do what it can to ensure that this rule is met.
So it only allows us to pass what we get back from the NativeCString function but no other things.
This way we can never generate code that is defect.

Even if i use the word 'lie' here it is nothing evil or forbidden that should never be done.
It simply ensures that the monkey enviroment works like you would expect it do to (Safety & Typechecking).

hope that helps.


Pharmhaus(Posted 2014) [#21]
OK, seems to work the half way now.
The image cannot be loaded because the path is wrong (took a while to realize that...).
fix:
		texture = dxLoadTexture(NativeCString(DataPath() + "\boing.jpg"), 1)


Function DataPath:String()
	#if HOST="winnt"
		Return (os.ExtractDir(AppPath()) + "/data").Replace("/", "\")
	#elseif HOST="macos"
		Return os.ExtractDir(AppPath()) + "/data"
	#elseif HOST="linux"
		Return os.ExtractDir(AppPath()) + "/data"
	#endif
End


This should work to get a String from CStrings.
FromCString:
String FromCString(const char* c)
{
	return String(&c[0]);
}



Function NativeCString:CStringNative(S:String)
Function FromCString:String(S:CStringNative)
		
Class CStringNative Abstract
End
	

I tinkered a bit around around and i think that Poppy's Library does not return null terminated Strings, but I'm not sure.
It works fine to set the Window title and load your texture, but the returned Strings from the Dll are always corrupted (more random character than you put in).
Print FromCString(NativeCString("hello world!")) 'Works fine
dxNameEntity(cube, NativeCString("Hello World!"))
Print FromCString(dxEntityName(cube))' Cruel special characters appearing 



Danilo(Posted 2014) [#22]
@DruggedBunny:
Modified your BlitzMax generator little bit, so you can use Monkey X Strings directly: wiredmonkey_new.zip (47k)

You can directly use:
texture = dxLoadTexture("data/boing.jpg", 1)

instead
texture = dxLoadTexture(NativeCString ("data/boing.jpg"), 1)

Works also with return values, for example:
Print dxGetWinVersion()

Although the return strings seem still not to be 0-terminated correctly.


Pharmhaus(Posted 2014) [#23]
I was just doing something similar but it looks like you were faster.
Sooo how about hard wired mojo next ? :D

EDIT: I will check some of the dll functions now.
EDIT2: There is a bug in danilos version download *This* instead.


DruggedBunny(Posted 2014) [#24]
Whoa, thanks guys! Need to read all this to try and take it in...

Did something change with regards to paths, though? I thought Desktop still worked the same, ie. media files in a project.data folder don't need a path?


Sooo how about hard wired mojo next ? :D


Ambitious!

I think the wrapper should stay pretty future-proof, anyway, given it's only reading the .decls file, and the format of that file is pretty much static... not looked at the changes yet, but it sounds like Pharmhaus's updated version is probably the 'final' version... ?

I also want to wrap Blitz3DSDK, but of course not very many people have access to that... :/

Also plan to port my crude BlitzMax code for accessing the Xbox360 pad's rumble effects via xinput.dll.

Thanks again, all, wanted to do this quite a while ago but found the C++ side way too daunting before!


Pharmhaus(Posted 2014) [#25]

Did something change with regards to paths, though? I thought Desktop still worked the same, ie. media files in a project.data folder don't need a path?


The problem is that Ploppy's dll is not a part of monkey and thus does not benefit from the internal monkey ability to manage that.


I think the wrapper should stay pretty future-proof, anyway, given it's only reading the .decls file, and the format of that file is pretty much static... not looked at the changes yet, but it sounds like Pharmhaus's updated version is probably the 'final' version... ?


Danilo did the magic here, I only fixed a bug that he overlooked. But yes the version from my post is the final one.


Thanks again, all, wanted to do this quite a while ago but found the C++ side way too daunting before!


It indeed is.... (Maybe because of the *good* documentation of mark's code?)

EDIT: please note that DataPath() should replace / with \ but the forum ate it.


DruggedBunny(Posted 2014) [#26]

The problem is that Ploppy's dll is not a part of monkey and thus does not benefit from the internal monkey ability to manage that.


Ahhh, well spotted!