13

Here is what I want: I have a huge legacy C/C++ codebase written for POSIX, including some very POSIX specific stuff like pthreads. This can be compiled on Cygwin/GCC and run as an executable under Windows with the Cygwin DLL.

What I would like to do is build the codebase itself into a Windows DLL that I can then reference from C# and write a wrapper around it to access some parts of it programatically.

I have tried this approach with the very simple "hello world" example at http://www.cygwin.com/cygwin-ug-net/dll.html and it doesn't seem to work.

#include <stdio.h>
extern "C" __declspec(dllexport) int hello();

int hello()
{
  printf ("Hello World!\n");
 return 42;
}

I believe I should be able to reference a DLL built with the above code in C# using something like:

[DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string dllToLoad);

[DllImport("kernel32.dll")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);

[DllImport("kernel32.dll")]
public static extern bool FreeLibrary(IntPtr hModule);


[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int hello();

static void Main(string[] args)
{
    var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "helloworld.dll");
    IntPtr pDll = LoadLibrary(path);
    IntPtr pAddressOfFunctionToCall = GetProcAddress(pDll, "hello");

    hello hello = (hello)Marshal.GetDelegateForFunctionPointer(
                                                                pAddressOfFunctionToCall,
                                                                typeof(hello));

    int theResult = hello();
    Console.WriteLine(theResult.ToString());
    bool result = FreeLibrary(pDll);
    Console.ReadKey();
}

But this approach doesn't seem to work. LoadLibrary returns null. It can find the DLL (helloworld.dll), it is just like it can't load it or find the exported function.

I am sure that if I get this basic case working I can reference the rest of my codebase in this way. Any suggestions or pointers, or does anyone know if what I want is even possible? Thanks.

Edit: Examined my DLL with Dependency Walker (great tool, thanks) and it seems to export the function correctly. Question: should I be referencing it as the function name Dependency Walker seems to find (_Z5hellov)? Dependency Walker Output

Edit2: Just to show you I have tried it, linking directly to the dll at relative or absolute path (i.e. not using LoadLibrary):

    [DllImport(@"C:\.....\helloworld.dll")]
    public static extern int hello();


    static void Main(string[] args)
    {
        int theResult = hello();
        Console.WriteLine(theResult.ToString());
        Console.ReadKey();
    }

This fails with: "Unable to load DLL 'C:.....\helloworld.dll': Invalid access to memory location. (Exception from HRESULT: 0x800703E6)

*****Edit 3: ***** Oleg has suggested running dumpbin.exe on my dll, this is the output:

Dump of file helloworld.dll

File Type: DLL

Section contains the following exports for helloworld.dll

00000000 characteristics
4BD5037F time date stamp Mon Apr 26 15:07:43 2010
    0.00 version
       1 ordinal base
       1 number of functions
       1 number of names

ordinal hint RVA      name

      1    0 000010F0 hello

Summary

    1000 .bss
    1000 .data
    1000 .debug_abbrev
    1000 .debug_info
    1000 .debug_line
    1000 .debug_pubnames
    1000 .edata
    1000 .eh_frame
    1000 .idata
    1000 .reloc
    1000 .text





Edit 4 Thanks everyone for the help, I managed to get it working. Oleg's answer gave me the information I needed to find out what I was doing wrong.

There are 2 ways to do this. One is to build with the gcc -mno-cygwin compiler flag, which builds the dll without the cygwin dll, basically as if you had built it in MingW. Building it this way got my hello world example working! However, MingW doesn't have all the libraries that cygwin has in the installer, so if your POSIX code has dependencies on these libraries (mine had heaps) you can't do this way. And if your POSIX code didn't have those dependencies, why not just build for Win32 from the beginning. So that's not much help unless you want to spend time setting up MingW properly.

The other option is to build with the Cygwin DLL. The Cygwin DLL needs an initialization function init() to be called before it can be used. This is why my code wasn't working before. The code below loads and runs my hello world example.

    //[DllImport(@"hello.dll", EntryPoint = "#1",SetLastError = true)]
    //static extern int helloworld(); //don't do this! cygwin needs to be init first

    [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
    static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

    [DllImport("kernel32", SetLastError = true)]
    static extern IntPtr LoadLibrary(string lpFileName);


    public delegate int MyFunction();

    static void Main(string[] args)
    {
        //load cygwin dll
        IntPtr pcygwin = LoadLibrary("cygwin1.dll");
        IntPtr pcyginit = GetProcAddress(pcygwin, "cygwin_dll_init");
        Action init = (Action)Marshal.GetDelegateForFunctionPointer(pcyginit, typeof(Action));
        init(); 

        IntPtr phello = LoadLibrary("hello.dll");
        IntPtr pfn = GetProcAddress(phello, "helloworld");
        MyFunction helloworld = (MyFunction)Marshal.GetDelegateForFunctionPointer(pfn, typeof(MyFunction));

        Console.WriteLine(helloworld());
        Console.ReadKey();
    }

Thanks to everyone that answered~~

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Dale
  • 12,884
  • 8
  • 53
  • 83
  • 2
    The tool Dependency Walker (http://www.dependencywalker.com/) is useful for making sure DLLs are loadable and looking at their dependencies. – WildCrustacean Apr 26 '10 at 00:18
  • are you certain that while helloworld.dll is in path and can be found, that the cyg*.dll files are also in path from the place you are running your c# assembly. If you are using Visual Studio, go in to the Project Properties and make sure you put all the required paths in to the "Environment Variable" property. For other ways to set the path, see http://stackoverflow.com/questions/428085/how-do-i-set-a-path-in-visual-studio – Fuzz Apr 26 '10 at 01:14
  • I don't understated why you want to call your cygwin DLL from C# in all your experiments. What advantages you have from the pragmatical side compares with using unmanaged C++? You needs already have POSIX subsystem loaded. Why do you need .NET runtime be loaded in the same process? One can communicate between Win32 process and managed process written in C#, but they must *not share the same address space* like you try to do now. – Oleg May 02 '10 at 23:35
  • Try looking in the system or application event log and see if there are any related messages which arrive at the time when you see that error message. – shoosh May 04 '10 at 21:04

5 Answers5

9

The main problem which you has is following. Before you can use your helloworld.dll a cygwin environment must be initialized (see http://cygwin.com/faq/faq.programming.html#faq.programming.msvs-mingw). So the following code in native C++ will works:

#include <windows.h>

typedef int (*PFN_HELLO)();
typedef void (*PFN_CYGWIN_DLL_INIT)();

int main()
{
    PFN_HELLO fnHello;
    HMODULE hLib, h = LoadLibrary(TEXT("cygwin1.dll")); 
    PFN_CYGWIN_DLL_INIT init = (PFN_CYGWIN_DLL_INIT) GetProcAddress(h,"cygwin_dll_init");
    init(); 

    hLib = LoadLibrary (TEXT("C:\\cygwin\\home\\Oleg\\mydll.dll"));
    fnHello = (PFN_HELLO) GetProcAddress (hLib, "hello");
    return fnHello();
}

Of cause the path to cygwin1.dll must be found. You can set C:\cygwin\bin as a current directory, use SetDllDirectory function or easy include C:\cygwin\bin in the global PATH environment variable (click on right mouse button on Computer, choose Properties then "Advanced System Settings", "Environment variables...", then choose system variable PATH and append it with ";C:\cygwin\bin").

Next if you compile you DLL, you should better to use DEF-file to define BASE address of DLL during compiling and makes all function names, which you exported more clear readable (see http://www.redhat.com/docs/manuals/enterprise/RHEL-4-Manual/gnu-linker/win32.html)

You can verify results with dumpbin.exe mydll.dll /exports, if you have Visual Studio installed. (don't forget start command promt from "Visual Studio Command Prompt (2010)" to have all Visual Studio set).

UPDATED: Because you don't write about the success I think there are exist some problems. In Win32/Win64 world (unmanaged world) it works. The code which I posted I have tested. Loading of CygWin DLLs in .NET can have some problem. In http://cygwin.com/faq/faq.programming.html#faq.programming.msvs-mingw one can read "Make sure you have 4K of scratch space at the bottom of your stack". This requirement can be wrong in .NET. A stack is a part of thread and not a process. So you can try to use CygWin DLLs in the new .NET Thread. Since .NET 2.0 one can define the maximum stack size for the thread. One other way is trying to understand http://cygwin.com/cgi-bin/cvsweb.cgi/~checkout~/src/winsup/cygwin/how-cygtls-works.txt?rev=1.1&content-type=text/plain&cvsroot=src and the code described in http://old.nabble.com/Cygwin-dll-from-C--Application-td18616035.html#a18616996. But the really interesting I find two ways without any tricks:

  1. Compiling the DLL with respect of MinGW tools instead of CygWin tools. MinGW produce code which are much more Windows compatible. I am not using CygWin or MinGW myself, so I am not sure, that you will be able to compile all you existing code used POSIX function in MinGW. If it is do possible, this way can have more success. You can look at http://www.adp-gmbh.ch/csharp/call_dll.html for example, to see, that MinGW DLL can be called from C# exactly like a Windows DLL.
  2. Usage of CygWin DLL inside of unmanaged process or unmanaged thread. This is a standard way described in CygWin documentation and it works (see example from my first post).

P.S. Please write short in the text of your question if you have success in one of this or in another way which you choose at the end. It's interesting for me independent on reputation and bounty.

Oleg
  • 220,925
  • 34
  • 403
  • 798
  • Hi Oleg, thanks for your help. Actually I haven't had time to try any other ways yet, but I will update this thread with my results. See my edited answer for the output of dumpbin.exe, the base address seems to be ok, and the function name is there too. Thanks for your suggestions I will try them in the next few days. – Dale May 05 '10 at 02:15
  • I had the same problem (POSIX-only library + P/Invoke). Note that the 4K free stack space should be at the bottom of the stack, that is, towards the high addresses. This requirement is impossible to satisfy once some stack frames were pushed to the stack. Thanks to the code you linked to, I created an application the load cygwin1.dll with proper stack and then hosts a CLR. However, due to other constraints, the P/Invoked dlls cannot do anything useful even this way (multiple C runtimes are not healthy in the same process). A note for anyone who wants to try this out: don't, it's pointless. – Kristóf Marussy Sep 24 '16 at 18:34
2

You should first try to get your simple hello world sample running. There is not much sense in trying with a huge legacy C/C++ codebase written for POSIX depending on Cygwin if you don't even get the former right. And please note that if you are linking Cygwin in you have to GPL-license your library.

For that have a look at the documentation (e.g. you need to specify explicitly in your hello world example if you are using Cdecl (what you currently don't do)): Consuming Unmanaged DLL Functions

On the native side you have to initialize Cygwin (see winsup/cygwin/how-cygtls-works.txt)

Use P/Invoke to your library directly. There is no sense in PInvoking into Win32 libraries like LoadLibrary to then invoke your libraries. This just adds another layer for errors and is gaining you exactly nothing.

Make sure you are getting the architecture right (.Net applications run 64bit by default on 64bit machines). So make sure your dlls match/support that or limit the .Net to 32bits.

If that works try getting your other library to work. (And you will have to have some luck if you expect to use functions that mix two entirely different threading models)

Foxfire
  • 5,675
  • 21
  • 29
  • Thanks, that's what I'm trying to do. If I can get this really basic example working, the hard part is over. I only tried the loadlibrary method after directly p/invoking failed, hoping it would give me a more descriptive error. – Dale May 03 '10 at 06:28
  • Then the next two steps are likely adding the cygwin initialization to your hello world c library and then adding the missing data to the DllImport Attribute in the hello world c# code. – Foxfire May 03 '10 at 10:56
1

The standard MS C compiler supports most of the POSIX interfaces including pthreads. Sometimes as separate implementations but usually as macros which convert the POSIX syntax to windows library calls.

If your C code doesn't have too many "gnuisms" you should be able to compile it using the standard Visual C compiler.

abatishchev
  • 98,240
  • 88
  • 296
  • 433
James Anderson
  • 27,109
  • 7
  • 50
  • 78
  • The POSIX subsystem is not really maintained anymore and there are some differences which may make porting a POSIX heavy code challenging. – shoosh May 04 '10 at 21:08
1

You should be able to reference the DLL that was built against Cygwin without having to create a new DLL. The only requirement is that you need to make sure that both "cygwin.dll" and the DLL that you are trying to load are in the appropriate paths. You probably need to use SetDllDirectory prior to invoking "LoadLibrary".

Michael Aaron Safyan
  • 93,612
  • 16
  • 138
  • 200
1

The code as written won't work, the exported function name is decorated by the C++ compiler and no longer resembles "hello". You'd normally declare the function extern "C" to suppress the decoration.

But, you haven't gotten that far yet. The LoadLibrary() call is failing with Windows error 998, ERROR_NOACCESS, "Invalid access to memory location". This most typically happens when the DllMain() entrypoint in one of the DLLs you have a dependency on bombs with an AccessViolation hardware exception. This should be visible in the debugger (Output window in Visual Studio), you should see a "First chance exception" with exception code 0xc0000005.

This is probably going to be unpleasant to diagnose, you've got several DLLs that are candidates and whose debugging symbols probably are a poor match for the debugger you use. Try to isolate this by writing a small test program in C that calls LoadLibrary. Configure the debugger to stop on the first chance exception. In Visual Studio you'd do this with Debug + Exceptions, Thrown checkbox. Good luck with it!

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536