Skip to content
This repository has been archived by the owner on Nov 1, 2020. It is now read-only.

Allow creating static or shared native library #4718

Merged
merged 34 commits into from
Nov 21, 2017

Conversation

tonerdo
Copy link
Contributor

@tonerdo tonerdo commented Oct 13, 2017

This PR adds support for creating static libraries (.lib, .a) and dynamic libraries (.dll, .dylib, .so) with CoreRT.

Commands

  • Dynamic library (Windows): dotnet build /t:LinkNative /p:NativeLib=Shared /p:DefFile=<path-to-def-exports-file>
  • Dynamic library (Unix): dotnet build /t:LinkNative /p:NativeLib=Shared
  • Static library: dotnet build /t:LinkNative /p:NativeLib=Static

dlopen on Unix and LoadLibrary on Windows are able to call any .NET method (within the generated native shared library) with the NativeCallable attribute applied. Generated static library (.lib, .a) would need to also be linked with the other CoreRT static libraries as well as your C++ code for all links to be resolved.

At this time there are errors when you try build a shared library on Ubuntu. Shared and static libraries can be successfully built on both Windows and macOS, only static libraries on Ubuntu

cc @jkotas @MichalStrehovsky

#1285

Copy link
Member

@MichalStrehovsky MichalStrehovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at the build system and bootstrapper changes in detail yet, but I like this!


public LibraryRootProvider(EcmaModule module)
{
_module = module;
}

public LibraryRootProvider(EcmaModule module, IList<MethodDesc> libraryInitializers)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this we should just create a separate ICompilationRootProvider instead (e.g. a NativeLibraryInitializerRootProvider that looks similar to the existing MainMethodRootProvider).

LibraryRootProvider would go over all the methods and types in the provided module and root them for compilation. What you need here is just a root for the Initialize method (the new root provider will do that) and a root for the NativeCallable methods (existing ExportedMethodsRootProvider does that). You'll get natural tree shaking that way (things that are unreachable don't get compiled).

_libraryInitializers = libraryInitializers;
}

protected override int ClassCode => -304225482;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We put ClassCode and CompareToImpl in separate dot files so that once we stand up a JIT (that doesn't need to sort) we can compile the JIT codebase without support for this. Granted, it's unlikely we would use this particular class in the JIT codebase, but we've been doing it everywhere for consistency.

}
}

// Simply return 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also just make this return void.

Copy link
Contributor Author

@tonerdo tonerdo Oct 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I return 0 to signify a success exit code. That way this line: https://github.com/tonerdo/corert/blob/static-shared-library/src/Native/Bootstrap/main.cpp#L388 will return the necessary value for the main function to continue

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this can never return failure exit code, it is not very useful for it to return success exit code...

Also, it would be nice to change this method to take no arguments since they are unused and always null. It should make some of the ifdefs in the boostrapper simpler.

else if (_nativeLib)
{
EcmaModule module = null;
// Get first (and only) module
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this limitation necessary? We could go over all of them and create a ExportedMethodsRootProvider for each.

@MichalStrehovsky
Copy link
Member

Also, could you sign the Contribution License Agreement at https://cla2.dotnetfoundation.org? I know you contributed in the past (with CLA signed), but something in the system thinks you didn't sign it yet.

@tonerdo
Copy link
Contributor Author

tonerdo commented Oct 13, 2017

@MichalStrehovsky I think it's because I changed my GitHub username. Signed it now

// Standard calling convention variant of RhpReversePInvoke
COOP_PINVOKE_HELPER(void, RhpReversePInvoke2, (ReversePInvokeFrame * pFrame))
{
// Entrypoint for reverse PInvoke
// ensure runtime is initialized
InitializeRuntime();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a hot fast path. The runtime initialization should to be done in RhpReversePInvokeAttachOrTrapThread2 - similar to how the thread initialization is done.


extern "C" int InitializeRuntime()
{
if (RUNTIME_INITIALIZED)
Copy link
Member

@jkotas jkotas Oct 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be thread safe. If one thread is initializing the runtime, the other thread should wait for it. I think the logic to do that should be in the Runtime, not in the bootstrapper.

@@ -1295,9 +1295,15 @@ EXTERN_C NOINLINE void FASTCALL RhpReversePInvokeAttachOrTrapThread2(ReversePInv
// PInvoke
//

extern "C" int InitializeRuntime();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This creates a static dependency from Runtime to bootstrapper. These dependencies are problematic. (E.g. for .NET Native on Windows UWP, we build runtime as separate .dll that does not include bootstrapper.)

Can InitializeRuntime here be a function pointer that the bootstrapper sets - e.g. in a static constructor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkotas sounds reasonable. Is there a part in the code where something similar is done? I might be able to get some ideas from there

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think there is anything like that in CoreRT today. There are number of examples out there on how to achieve it: https://www.bing.com/search?q=__attribute__%20((constructor))

@jkotas
Copy link
Member

jkotas commented Oct 13, 2017

I'm unable to get a CoreRT to build on Ubuntu

What's your Ubuntu version - what are the errors you are getting?

@tonerdo
Copy link
Contributor Author

tonerdo commented Oct 13, 2017

@jkotas I'm using Ubuntu 14.04 in a docker container running on macOS. Very little helpful error message, just says that clang exited with a non-zero code

@tonerdo
Copy link
Contributor Author

tonerdo commented Oct 16, 2017

@jkotas my bad I've now been able to build CoreRT on Ubuntu. However, trying to build a shared library fails (static library works). Will address comments once I fix that

@tonerdo tonerdo force-pushed the static-shared-library branch 2 times, most recently from aa4a40e to 20b1280 Compare October 23, 2017 11:54
// Standard calling convention variant and actual implementation for RhpReversePInvokeAttachOrTrapThread
EXTERN_C NOINLINE void FASTCALL RhpReversePInvokeAttachOrTrapThread2(ReversePInvokeFrame * pFrame)
{
// Entrypoint for reverse PInvoke
// ensure runtime is initialized
InitializeRuntime();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would push it even lower into Thread::ReversePInvokeAttachOrTrapThread to the path that is initializing the thread.

@dotnet dotnet deleted a comment from dnfclas Nov 1, 2017
@dotnet dotnet deleted a comment from dnfclas Nov 1, 2017
@@ -39,6 +39,8 @@

typedef uint32_t BOOL;
typedef uint32_t DWORD;
typedef HANDLE HINSTANCE;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be added here.


#ifdef CORERT_DLL
#if defined(_WIN32)
extern "C" BOOL WINAPI DllMain(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoever is linking the static library may want to have custom DllMain too. This will make it impossible.

Have you tried using static constructor for the initialization?

static struct InitializeRuntimePointerHelper
{
	InitializeRuntimePointerHelper()
	{
    ...
	}
} initializeRuntimePointerHelper;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkotas won't this mean that there'll still be a static dependency on the bootstrapper (e.g. with .NET Native on Windows UWP)? Would this static construct be run when this library is loaded?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dependencies from runtime to the bootstrapper are the problematic ones. If you add the static constructor to the boostrapper, I do not see any problems with dependencies.

@tonerdo
Copy link
Contributor Author

tonerdo commented Nov 11, 2017

All comments addressed. I'm still looking into the build failure for shared libraries on Ubuntu, although at this point it could be an issue on its own.

cc @jkotas @MichalStrehovsky

@@ -1118,7 +1121,15 @@ FORCEINLINE bool Thread::InlineTryFastReversePInvoke(ReversePInvokeFrame * pFram
void Thread::ReversePInvokeAttachOrTrapThread(ReversePInvokeFrame * pFrame)
{
if (!IsStateSet(TSF_Attached))
{
if (*InitializeRuntimePtr != NULL && RuntimeInitializingThread != this)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not thread safe. If two threads happen to be first to call a managed method at the same time, both of them will start initializing the runtime and bad things will happen. There needs to be logic to wait for the initialization to finish that will kick in for the second thread. A spin wait loop is probably the easiest way to do that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is problem even without the race actually. The RuntimeInitializingThread != this condition will be false for the second thread calling here and the initialization will be done second time.

}

#ifdef CORERT_DLL
static struct InitializeRuntimePointerHelper
Copy link
Member

@jkotas jkotas Nov 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work with optimizations on? I would think there needs to be something to keep the static constructor alive, e.g. linker directive on Windows - pragma(comment(linker,"/include... mentioned in one of the articles I linked earlier.

@@ -33,6 +33,9 @@
EXTERN_C REDHAWK_API void* REDHAWK_CALLCONV RhpHandleAlloc(void* pObject, int type);
EXTERN_C REDHAWK_API void REDHAWK_CALLCONV RhHandleFree(void*);

EXTERN_C int (*InitializeRuntimePtr)() = NULL;
Copy link
Member

@jkotas jkotas Nov 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would look better - more consistent with the overall style of the CoreRT runtime - to keep the pointer static without EXTERN_C, and expose the setter as a method - like RhSetRuntimeInitializationCallback - that is called from the bootstrapper.

@@ -33,6 +33,9 @@
EXTERN_C REDHAWK_API void* REDHAWK_CALLCONV RhpHandleAlloc(void* pObject, int type);
EXTERN_C REDHAWK_API void REDHAWK_CALLCONV RhHandleFree(void*);

EXTERN_C int (*InitializeRuntimePtr)() = NULL;
static Thread* RuntimeInitializingThread;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: g_ prefix maybe nice (I know that we are not 100% consistent).

if (*InitializeRuntimePtr != NULL && RuntimeInitializingThread != this)
{
RuntimeInitializingThread = this;
(*InitializeRuntimePtr)();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set the RuntimeInitializingThread to NULL once you are done?

@@ -293,15 +293,58 @@ static const pfn c_classlibFunctions[] = {
#endif // !CPPCODEGEN

extern "C" void InitializeModules(void* osModule, void ** modules, int count, void ** pClasslibFunctions, int nClasslibFunctions);
extern "C" int InitializeRuntime();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can move the implementation of InitializeRuntime up to avoid the forward declaration.

int retval;
#ifdef CPPCODEGEN
try
#endif
{
#ifndef CORERT_DLL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in #ifndef CORERT_DLL already

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, thanks!

@@ -33,6 +33,9 @@
EXTERN_C REDHAWK_API void* REDHAWK_CALLCONV RhpHandleAlloc(void* pObject, int type);
EXTERN_C REDHAWK_API void REDHAWK_CALLCONV RhHandleFree(void*);

static int (*InitializeRuntimePtr)();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: g_RuntimeInitializationCallback

@tonerdo
Copy link
Contributor Author

tonerdo commented Nov 13, 2017

Does this work with optimizations on? I would think there needs to be something to keep the static constructor alive, e.g. linker directive on Windows - pragma(comment(linker,"/include... mentioned in one of the articles I linked earlier.

A normal build on Windows works just as well as on Nix. How do I turn on optimizations?

@jkotas
Copy link
Member

jkotas commented Nov 13, 2017

How do I turn on optimizations?

Use Release build of the runtime: "build.cmd Release"

@Fabi
Copy link

Fabi commented Nov 17, 2017

Is there any special Method called from the DllMain method in the native dll on Windows? Like idk a Main equivalent method in the managed code which is called when DllMain is called?

@jkotas
Copy link
Member

jkotas commented Nov 17, 2017

Is there any special Method called from the DllMain method in the native dll on Windows?

There is not, and we do not plan to add one. It is not a good idea to execute managed code during DllMain - it leads to deadlocks with Windows loader lock.

@tonerdo
Copy link
Contributor Author

tonerdo commented Nov 20, 2017

Does this work with optimizations on? I would think there needs to be something to keep the static constructor alive, e.g. linker directive on Windows - pragma(comment(linker,"/include... mentioned in one of the articles I linked earlier.

I can confirm that this works with optimizations on (release build of CoreRT)

@Fabi
Copy link

Fabi commented Nov 20, 2017

Is generating exports working right now? I built a simple dll file with
[NativeCallable] public static int TestFunction(int first, int second) { return first+ second; }

However there is no export created for that single functions.

@MichalStrehovsky
Copy link
Member

I built a simple dll file with

@Fabi You'll want to set the entrypoint name. Otherwise you get a function that is native callable (you can take a function pointer to it and give it to native code and it will work), but doesn't have a symbolic name.

Don't forget to also pass a def file, if you're on Windows.

void Thread::ReversePInvokeAttachOrTrapThread(ReversePInvokeFrame * pFrame)
{
if (!IsStateSet(TSF_Attached))
{
if (*InitializeRuntimePtr != NULL && g_RuntimeInitializingThread != this)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make this thread-safe, you can use something like this:

...
    if (g_RuntimeInitializationCallback != NULL && g_RuntimeInitializingThread != this)
    {
        EnsureRuntimeInitialized();
    }
...



void Thread::EnsureRuntimeInitialized()
{
    while (PalInterlockedCompareExchangePointer(&g_RuntimeInitializingThread, this, NULL) != NULL)
    {
        PalSleep(1);
    }

    if (g_RuntimeInitializationCallback != NULL)
    {
        g_RuntimeInitializationCallback();
        g_RuntimeInitializationCallback = NULL;
    }

    PalInterlockedExchangePointer(&g_RuntimeInitializingThread, NULL);
}

#ifdef CORERT_DLL
static struct InitializeRuntimePointerHelper
{
InitializeRuntimePointerHelper()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation is off here. Tabs vs. spaces?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces 😄

extern "C" void __managed__Startup();
#endif // !CORERT_DLL

int InitializeRuntime()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static int InitializeRuntime()


if (g_RuntimeInitializationCallback != NULL)
{
g_RuntimeInitializationCallback();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback returns int, but the return value is not checked. Change the return value of the callback to void?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or keep it as int, check the return and fail fast it is non-zero.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going with the second option, makes sense to act when runtime initialization fails

@@ -310,7 +315,11 @@ int main(int argc, char* argv[])
#endif // CPPCODEGEN

#ifndef CPPCODEGEN
#ifndef CORERT_DLL
void * osModule = PalGetModuleHandleFromPointer((void*)&__managed__Main);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#define CORERT_ENTRYPOINT __managed__Main / #define CORERT_ENTRYPOINT __managed__Startup in the ifdef above, so that we do not need to have ifdef here?

@jkotas
Copy link
Member

jkotas commented Nov 21, 2017

Windows build is failing with error C2664: 'void *PalInterlockedCompareExchangePointer(void *volatile *,void *,void *)': cannot convert argument 1 from 'Thread **' to 'void *volatile *'. Need to cast the first argument to make it happy.

@jkotas
Copy link
Member

jkotas commented Nov 21, 2017

This looks good to me, modulo the last set of comments. Thank you for pushing on this!

@jkotas
Copy link
Member

jkotas commented Nov 21, 2017

@MichalStrehovsky Could you please take a final look at this as well?

Copy link
Member

@MichalStrehovsky MichalStrehovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, apart from the nits.

We'll also want to do something about the library initializers (we probably don't need to inject System.Private.DeveloperExperience.Console into a non-console app), but that can be fixed in subsequent pull requests.


using Internal.TypeSystem;

using AssemblyName = System.Reflection.AssemblyName;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: unneeded using


using Internal.TypeSystem;

using AssemblyName = System.Reflection.AssemblyName;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: unneeded using

@tonerdo
Copy link
Contributor Author

tonerdo commented Nov 21, 2017

All done!!!

@jkotas jkotas merged commit 8d0f5ed into dotnet:master Nov 21, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants