Skip to content

Trying to load a self-contained dll, with an own runtime-host #35329

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
unglaublicherdude opened this issue Apr 23, 2020 · 13 comments
Closed

Trying to load a self-contained dll, with an own runtime-host #35329

unglaublicherdude opened this issue Apr 23, 2020 · 13 comments
Labels
area-Host feature-request untriaged New issue has not been triaged by the area owner
Milestone

Comments

@unglaublicherdude
Copy link

unglaublicherdude commented Apr 23, 2020

Hi,

I am trying to get a project running, where I don't want to rely on a installed framework on the users system. So I tried the following setup:

My hosting-code:

#include <iostream>

#include "pch.h"
#include <windows.h>
#include <assert.h>
#include <string>
#include <ShlObj.h>
#include "nethost.h"
#include "hostfxr.h"
#include "coreclr_delegates.h"

using std::wstring;

#define STR(s) L ## s
#define CH(c) L ## c
#define DIR_SEPARATOR L'\\'

namespace
{
    void load_lib(std::string lib_name)
    {
        auto load_lib_result = LoadLibraryA(lib_name.c_str());
        if (load_lib_result != NULL)
        {
            std::string debug_message = "success loading -> " + lib_name;
            printf(debug_message.c_str());
            FreeLibrary(load_lib_result);
        }
        else
        {
            int errorCode = GetLastError();
            std::string debug_message = "loading " + lib_name + " " + std::to_string(errorCode) + " failed";
            printf(debug_message.c_str());
        }
    }

    
    hostfxr_initialize_for_runtime_config_fn init_fptr;
    hostfxr_get_runtime_delegate_fn get_delegate_fptr;
    hostfxr_close_fn close_fptr;

    void* load_library(const char_t*);
    void* get_export(void*, const char*);

    void* load_library(const char_t* path)
    {
        HMODULE h = ::LoadLibraryW(path);
        assert(h != nullptr);
        return (void*)h;
    }
    void* get_export(void* h, const char* name)
    {
        void* f = ::GetProcAddress((HMODULE)h, name);
        assert(f != nullptr);
        return f;
    }
}

load_assembly_and_get_function_pointer_fn get_dotnet_load_assembly(const char_t* config_path)
{
    void* load_assembly_and_get_function_pointer = nullptr;
    hostfxr_handle cxt = nullptr;

    hostfxr_initialize_parameters parameters{
        sizeof(hostfxr_initialize_parameters),
        L".\\",
        L".\\"
    };
	
    int rc = init_fptr(config_path, &parameters, &cxt);
    if (rc > 1 || cxt == nullptr)
    {
        std::string error_message = "Init failed with rc: " + std::to_string(rc);
        printf(error_message.c_str());
        close_fptr(cxt);
        return nullptr;
    }

    rc = get_delegate_fptr(
        cxt,
        hdt_load_assembly_and_get_function_pointer,
        &load_assembly_and_get_function_pointer);
    if (rc != 0 || load_assembly_and_get_function_pointer == nullptr) {
        std::string error_message = "Get delegate failed with rc: " + std::to_string(rc);
        printf(error_message.c_str());
    }

    close_fptr(cxt);
    return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer;
}

bool load_hostfxr()
{
    char_t buffer[MAX_PATH] = L".\\hostfxr.dll";

    // Load hostfxr and get desired exports
    void* lib = load_library(buffer);
    init_fptr = (hostfxr_initialize_for_runtime_config_fn)get_export(lib, "hostfxr_initialize_for_runtime_config");
    get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate");
    close_fptr = (hostfxr_close_fn)get_export(lib, "hostfxr_close");

    return (init_fptr && get_delegate_fptr && close_fptr);
}

namespace
{
    bool isInitialized = false;
    component_entry_point_fn moduleStart = nullptr;
    component_entry_point_fn moduleStop = nullptr;

    int init()
    {
        printf(">> DnsCLoudClientEntry::init\n");

        if (!isInitialized)
        {
            if (!load_hostfxr())
            {
                printf("<< DnsCLoudClientEntry::init exit with failure in load_hostfxr()\n");
                assert(false && "Failure: load_hostfxr()");
                return EXIT_FAILURE;
            }

            wstring workingDirectory = L".\\";

            const wstring config_path = workingDirectory + STR("ElTesto.runtimeconfig.json");
            load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer = nullptr;
            load_assembly_and_get_function_pointer = get_dotnet_load_assembly(config_path.c_str());
            assert(load_assembly_and_get_function_pointer != nullptr && "Failure: get_dotnet_load_assembly()");

            const wstring dotnetlib_path = workingDirectory + STR("ElTesto.dll");
            const char_t* dotnet_type = STR("ElTesto.ElTesto, ElTesto");

            const char_t* dotnet_type_method = STR("FooBar");
            int rc = load_assembly_and_get_function_pointer(
                dotnetlib_path.c_str(),
                dotnet_type,
                dotnet_type_method,
                nullptr,
                nullptr,
                (void**)&moduleStart);
            assert(rc == 0 && moduleStart != nullptr && "Failure: load_assembly_and_get_function_pointer() ModuleStart");

            isInitialized = true;
        }
        printf("<< DnsCLoudClientEntry::init\n");
        return 0;
    }
}

int main()
{
    init();
    struct lib_args
    {
        const char_t* message;
        int number;
    };
    lib_args args{ STR(""), 1 };
    moduleStart(nullptr, 0);
}

The C# library has no dependencies besides the .Net Core runtime. I build it with this command line-command:
dotnet publish -r win-x64 -c release -f netcoreapp3.1 --self-contained

Now two questions:

  1. The publish command generates a runtimeconfig like this
{
  "runtimeOptions": {
    "tfm": "netcoreapp3.1",
    "includedFramework": [
      {
        "name": "Microsoft.NETCore.App",
        "version": "3.1.3"
      }
    ]
  }
}

Running the wrapper with that configuration I get an Error:
Initialization for self-contained components is not supported

Changing the runtimeconfig to this makes it run:

{
  "runtimeOptions": {
    "tfm": "netcoreapp3.1",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "3.1.3"
    }
  }
}

But I still have the problem, the it isn't fully self-contained, because while calling hostfxr_initialize_for_runtime_config it will always try getting some files system folders. Do i just missunderstand self-contained?

@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-System.Runtime untriaged New issue has not been triaged by the area owner labels Apr 23, 2020
@unglaublicherdude
Copy link
Author

unglaublicherdude commented Apr 23, 2020

We got it working in a folder structure like this:

release/ourEntry.dll
release/shared/Microsoft.NETCore.App/3.1.3/*all other dlls*

The main problem is, that it tries to load the hostpolicy.dll from the following path:
C:\Program Files (x86)\coreservicing\pkgs\runtime.win-x64.Microsoft.NETCore.DotNetHostPolicy\3.1.3\runtimes\win-x64\native\hostpolicy.dll
and then finds it in our release-path. Why does it look in the coreservicing-path?

@ghost
Copy link

ghost commented Apr 23, 2020

Tagging subscribers to this area: @vitek-karas, @swaroop-sridhar
Notify danmosemsft if you want to be subscribed.

@unglaublicherdude
Copy link
Author

We now have this folder structure:

release/ourEntry.dll (c++ wrapper/custom host code)
release/host/fxr/3.1.3/hostfxr.dll
release/shared/Microsoft.NETCore.App/3.1.3/ourDotNet.dll
release/shared/Microsoft.NETCore.App/3.1.3/*all other framework dlls*

We basically copied the output from publish --self-contained + the installed files, from ProgramFiles into the shared folder.

We now we tourned all servicable flags from true to false in the default Microsoft.NETCore.App.deps.json.

This makes our setup work without, trying to load anything from any other folder.

Question: Is this a doable way? We are curious if there is another way than copying that many files and manually editing the Microsoft.NETCore.App.deps.json.

@vitek-karas
Copy link
Member

Currently we don't support loading "self-contained components" into native hosts. In fact we don't support self-contained components - full stop (anywhere really). That's why you get the error message Initialization for self-contained components is not supported.

What I don't understand is why do you need to write your own native host in this case?

  • Why not just a simple self-contained managed app?
  • Even if you do need a custom native host, you can use the hostfxr_initialize_for_dotnet_command_line which should work with self-contained apps. But I would still be very interested why you need the custom native host in this case.

@unglaublicherdude
Copy link
Author

unglaublicherdude commented Apr 23, 2020

Thank you, for your answer. We are running in a PPL (Protected Process Light) context. Getting loaded by an already registered process (this is also the reason, why we need to have in our hand, which DLLs get loaded while running our component).

So what we do is: We have a process, that is registered as PPL. This process does load a native DLL (in our case, the custom host), that then does run our .Net Component.

The problem with the PPL context is, that we have to define certificate signatures that get accepted in our PPL context. So we cannot rely on a framework version that is installed on the system, because the certificate can change even in patch-level updates, which would break our component.

The setup we actually have is working, by basically mirroring the folder structure of the dotnet installation path and copying all the framework DLLs over and then editing the Microsoft.NETCore.App.deps.json to not allow servicable DLLs.

@vitek-karas
Copy link
Member

If you only want to run one component (so it's basically an app in that sense) I would strongly suggest you use the hostfxr_initialize_for_dotnet_command_line. That will let you load a self-contained runtime and the app and run it via hostfxr_run_app. The only difference is that once the run function returns the CLR has been effectively shut down and it expects that the process will soon exit (you can't run a second app in the same process anymore).

There's another possibility which is to install your own copy of the runtime. You can download a zip (instead of an installer) of the runtime, unzip to any location you want. Then run the app either via the dotnet.exe in the runtime, or if you still need native hosting just point it to the folder in the initialization structure. If you want to be 100% sure also set DOTNET_MULTILEVEL_LOOKUP=0 in that process. Then you can have framework dependent apps/components/whatever but you still control the runtime itself. It's basically very similar to what you ended up doing above, but a cleaner and more supported way.

@unglaublicherdude
Copy link
Author

Thank you again. We will try both of these ways. I think the DOTNET_MULTILEVEL_LOOKUP is the part of the puzzle we were missing.

@unglaublicherdude
Copy link
Author

I basically did that now, with setting the DOTNET_MULTILEVEL_LOOKUP. But in the ZIP-Package of the .Net runtime there are DLLs still marked as servicable. So what happens is, that the host will still look in the servicing-path first.

hostpolicy dll
other dll

load_assembly_and_get_function_pointer_fn get_dotnet_load_assembly(const char_t* config_path)
{
    void* load_assembly_and_get_function_pointer = nullptr;
    hostfxr_handle cxt = nullptr;

    hostfxr_initialize_parameters parameters{
        sizeof(hostfxr_initialize_parameters),
        L"C:\\myarbitaryapppath\\",
        L"C:\\mycustomruntimepath\\dotnet\\"
    };
	
    int rc = init_fptr(config_path, &parameters, &cxt);

}
bool load_hostfxr()
{
    // char_t buffer[MAX_PATH] = L".\\hostfxr.dll";

    // Pre-allocate a large buffer for the path to hostfxr
    char_t buffer[MAX_PATH];
    size_t buffer_size = sizeof(buffer) / sizeof(char_t);

    get_hostfxr_parameters parameters{
        sizeof(hostfxr_initialize_parameters),
        nullptr,
        L"C:\\mycustomruntimepath\\dotnet"
    };

    int rc = get_hostfxr_path(buffer, &buffer_size, &parameters);
    if (rc != 0)
        return false;

    // Load hostfxr and get desired exports
    void* lib = load_library(buffer);
    init_fptr = (hostfxr_initialize_for_runtime_config_fn)get_export(lib, "hostfxr_initialize_for_runtime_config");
    get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate");
    close_fptr = (hostfxr_close_fn)get_export(lib, "hostfxr_close");

    return (init_fptr && get_delegate_fptr && close_fptr);
}
    int init()
    {
        SetEnvironmentVariable(TEXT("DOTNET_MULTILEVEL_LOOKUP"), TEXT("0"));

        printf(">> DnsCLoudClientEntry::init\n");

        if (!isInitialized)
        {
            if (!load_hostfxr())
            {
                printf("<< DnsCLoudClientEntry::init exit with failure in load_hostfxr()\n");
                assert(false && "Failure: load_hostfxr()");
                return EXIT_FAILURE;
            }

            wstring workingDirectory = L"C:\\myarbitaryapppath\\";

            const wstring config_path = workingDirectory + STR("ElTesto.runtimeconfig.json");
            load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer = nullptr;
            load_assembly_and_get_function_pointer = get_dotnet_load_assembly(config_path.c_str());
            assert(load_assembly_and_get_function_pointer != nullptr && "Failure: get_dotnet_load_assembly()");

This is only prevented by also editing the Microsoft.NETCore.App.deps.json in my dotnet path C:\mycustomruntimepath\dotnet\shared\Microsoft.NETCore.App\3.1.3.

According to this Readme I think this will always happen. Which should be ok because we have anyways have to deploy the whole framework by ourselfs, so we can just edit the deps.json.

For the hostpolicy.dll I also confirmed that behaviour by this code. The servicing path is the first that is tried by the resolver and can basically just be avoided by removing the version in the deps.json or - but that would be even more hacky overwriting the ProgramFiles(x86) environment var with a path that is also under our control.

@vitek-karas
Copy link
Member

Right - the servicing (also called hammer servicing) is intentionally not easy to disable. It's for cases were there is a serious security problem which we would need to fix. Note that it is VERY version specific (it will only kick in if the exact version is available in the servicing path). And AFAIK it has never been used. And I can't imagine us using this and changing the code signing cert in such an update.

But if you can't live with that, the only way out really is modifying the .deps.json, or you could build your own hostpolicy and ship that - in a way you're sort of half way to that option already (modifying the framework's .deps.json gets you well into the uncharted/unsupported waters).

Still curious: Why do you need to use initialize_for_runtimeconfig and load_assembly_and_get_function_pointer - not that it won't work, but it seems you're trying to run an app (not load a component into a longer running process).

@unglaublicherdude
Copy link
Author

I'll try to give you some more information that's understandable. We have a already existing PPL process (this part, we cannot touch), that load and unloads many components (all other components are in C++ written DLLs) with our component beeing the first written in .Net. So this loader service (which is a long running service) would load our custom host (the native DLL) (which also would be long running). By long running I mean as long as the system is running or someone decides to deactivate the component.

When I understand your explanation correct, the hostfxr_initialize_for_dotnet_command_line wouldn't work in this scenario either because the process that starts and stops our component isn't stopped, when we as a single component are stopped. So the scenario that someone deactivates our component and activates it again while the "motherprocess" still lives, is a real scenario.

@vitek-karas
Copy link
Member

Thanks for the details. If ultimately this scenario can lead to a case where the native host (motherprocess) can activate the managed component, then deactivate it and try to activate some other managed component again, then such solution would not work with self-contained.

Currently we don't support unloading the CoreCLR from a process, or allowing two different CoreCLR runtimes in the same process. So supporting two self-contained components won't work.

The framework dependent approach is possible in this case as long as all the potentially loaded managed components can agree on one runtime. The first component will load the runtime which will be active in the process for the remainder of its lifetime. In the future other components can use it (but not to load another one).

@vitek-karas
Copy link
Member

There's a related discussion on a very similar topic here: #35465.

@vitek-karas vitek-karas added feature-request and removed untriaged New issue has not been triaged by the area owner labels May 11, 2020
@vitek-karas vitek-karas added this to the Future milestone May 15, 2020
@vitek-karas vitek-karas added the untriaged New issue has not been triaged by the area owner label May 15, 2020
@vitek-karas vitek-karas removed this from the Future milestone May 15, 2020
@agocke agocke added this to the Future milestone Jun 29, 2020
@vitek-karas
Copy link
Member

If you're going to load managed code just once into the process then I think #35465 is a solution for this issue. If you need to load multiple components of which some may be self-contained, that is currently not supported - we currently don't have plans to support loading multiple runtimes into the same process.

@ghost ghost locked as resolved and limited conversation to collaborators Dec 9, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-Host feature-request untriaged New issue has not been triaged by the area owner
Projects
None yet
Development

No branches or pull requests

5 participants