Skip to content
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

Making CefSharp compatible with non-default AppDomains #1556

Closed
wants to merge 6 commits into from

Conversation

arsher
Copy link
Contributor

@arsher arsher commented Jan 23, 2016

I had an idea about how to solve the non-default AppDomain problems with a very few effective modifications in the current code, using code generation. I took a look at issues #1488 and #351 and as far as I can tell the main concern against such changes was the possible extent of them. I think I managed to minimalize this by using libclang to parse the CEF headers and generate AppDomain safe wrappers around the relevant CEF interfaces. What do you guys think? Is this something that can be merged at some point?

Please take this for a spin, I did some testing of my own, but honestly only for the features relevant to me so there still might something off. This shouldn't change anything at all when used like the same way as before, and when not in the default appdomain it should only add the perf penalty of the cross-appdomain calls.

@amaitland
Copy link
Member

@arsher Another interesting PR! Unfortunately I don't have time to go over anything so complex at the moment. If someone else wants to step in then great, otherwise this may have to sit for a while, sorry.

@mountgellert
Copy link

This is awesome! When this is going to be merged? Please make it happen 👍

@markusdosch
Copy link

This is really great news. I'll try to build this on myself while it makes its way to the master :-)

@markusdosch
Copy link

After building it myself, the AppDomain problem I had some months ago (see www.stackoverflow.com/questions/31512997/cefsharp-within-addin-cannot-pass-a-gchandle-across-appdomains) is solved. Thanks a lot, @arsher! :-)

@mountgellert
Copy link

Hurray! :D

@amaitland
Copy link
Member

Previously it wasn't possible to write unit tests with any of the mainstream frameworks as they would spawn a new AppDomain, so this change makes using a framework like xUnit.Net possible. It really comes down to the question, Is this production ready?, like all changes, testing is required. As it's now possible to write unit tests, it seems logical to do so.

@arsher Thoughts?

@mountgellert @Sukram21 If you can, contribute what time you have if you require this feature. It's not a priority with me and my time being very limited at the moment I'm not going to commit much time to this.

@mountgellert
Copy link

@arsher, what kind of help you need for this to make to master?

@papertape
Copy link

I use VC# 2013 together with VS unit testing framework. I am unable to run a unit test for CEF dependent code until this change makes it to the next CEF release. My current CEF is 47.0.2

@amaitland
Copy link
Member

I use VC# 2013 together with VS unit testing framework.

Take it for a spin, compile your own version, test it out. Write some Unit Tests for the framework.

I am unable to run a unit test for CEF dependent code until this change makes it to the next CEF release.

Code won't be merged until it's been adequately tested, there are no plans to include this in any future releases yet.

@jepp
Copy link

jepp commented Mar 16, 2016

Code won't be merged until it's been adequately tested, there are no plans to include this in any future releases yet.

I'm frustrated that this isn't getting into main branch quickly, but I completely understand why. I'm going to try building and using this branch myself, but I know that's not the same as unit testing. Unfortunately, my lack of expertise in both cefsharp and unit tests for C# are limiting my helpfulness. But I just wanted to add my interest and note that one other person will be banging on it to find any cracks.

Very much appreciate you putting the time into this, arsher.

@amaitland
Copy link
Member

I'm frustrated that this isn't getting into main branch quickly

If you need an immediate solution then CefSharp is not the only option, you could use CefGlue or ChromiumFx, neither of which have this limitation.

@seveves
Copy link

seveves commented Aug 4, 2016

I would really appreciate it if this bugfix could make it to the master branch. @arsher please let me know if I can help with writing unit tests etc.

@varigence
Copy link

I am also using this in a custom build and find it to be solid. I'm not acquainted with the testing standards and other acceptance thresholds for this project. I'd be happy to take direction and help with whatever is needed to get this in, though.

@amaitland
Copy link
Member

@seveves @varigence To get this merged it really is a lot more than just creating some unit tests

There are unanswered questions

  • Who is going to maintain this feature? Answer support questions? Fix bugs? Really provide long term support. Are you both going to spend your own time fixing problems?
  • Is it wise to use the DSerfozo.LibclangSharp package? Whilst it's quite the technical achievement, it's not documented, no signs of an active community, hasn't been updated in a long time. Should a more mature libclang wrapper be chosen? Or another option?

The practical aspects:

  • Merge in current master and resolve any merge conflicts
  • Fix the naming, Safe doesn't mean anything, pick a more descriptive name
  • Add documentation on how all the pieces fit together and how it can be debugged (same goes for changing to another libclang wrapper).
  • Add extensive unit tests
  • Update the CI environment to run said units tests on every build

There are probably a lot of other steps that will be added along the way. Personally I have no requirement for those feature and extremely limited time (to the point where someone else would need to take charge of the code reviews as well),

@shanadas
Copy link

This something I have hit on and wondering whether this change made it into master? I no good in that concept detailed in this ticket, so would you please help me answer this? Do you advise this fix to be part of a production enviornment?

@jankurianski
Copy link
Member

@shanadas This page is a Pull Request so at the top of the page you can see if it has been merged (it has not).

From the initial description this PR was only done for the personal interest of the author and is not ready for production environments and has not been fully tested. Given that the PR was submitted 8 months ago, I recommend anyone wanting to see this change make its way into master to fork the author's branch and start addressing the issues detailed above by @amaitland. This is such a big change that it needs a new motivated individual to take the lead and stay active on Github to address future issues as the change makes its way into releases.

For now you will need to use another framework like CefGlue or ChromiumFx if you need to host Cef in a non-default AppDomain.

@flole
Copy link

flole commented Aug 27, 2016

This week I ran into the same problem with the GCHandel and multiple AppDomains. In my case I want to render an html file which contains a d3 chart in an ASP.NET WebApi. Because the default appdomain is owned by the IIS itself I couldn't do that.

After I looked at the code from @arsher I saw that the unmanaged part of CefSharp talks only with the default appdomain, which is in my case the IIS. My workaround for this is easy, let me explain:

I have a class called CefSharpRenderer which looks like this. What it does is simple. It initializes Cef when it isn't already, then it creates a new ChromiumWebBrowser and attach some events. When the browser is initialized Google is loaded. For my workaround it is necessary that this class inherits the MarshalByRefObject (why comes later). Also this class implements the method void RenderSomething(); which is defined by the ICefSharpRenderer.

public class CefSharpRenderer : MarshalByRefObject, ICefSharpRenderer
{
    private ChromiumWebBrowser _browser;
    private SemaphoreSlim _renderingFinishedSemaphore = new SemaphoreSlim(0, 1);
    public void RenderSomething()
    {
        if (!Cef.IsInitialized)
        {
            var settings = new CefSettings();

            var osVersion = Environment.OSVersion;
            //Disable GPU for Windows 7
            if (osVersion.Version.Major == 6 && osVersion.Version.Minor == 1)
            {
                // Disable GPU in WPF and Offscreen examples until #1634 has been resolved
                settings.CefCommandLineArgs.Add("disable-gpu", "1");
            }

            //Perform dependency check to make sure all relevant resources are in our output directory.
            Cef.Initialize(settings, shutdownOnProcessExit: true, performDependencyCheck: false);
        }


        _browser = new ChromiumWebBrowser();
        _browser.BrowserInitialized += _browser_BrowserInitialized;
        _browser.LoadingStateChanged += _browser_LoadingStateChanged;

        _renderingFinishedSemaphore.Wait();
    }

    private void _browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
    {
        if (e.IsLoading)
        {
            return;
        }

        //Google has been loaded
        //Yay!
        _renderingFinishedSemaphore.Release();
    }

    private void _browser_BrowserInitialized(object sender, EventArgs e)
    {
        _browser.Load("http://www.google.de");
    }
}

To make my workaround transparent to the caller I have an additional class which also implements the ICefSharpRenderer interface. This class is called CefSharpRendererProxy and contains the main work. When the RenderSomething() method is called it retrieves all appdomains from the current process with the GetAppDomains method (I found this method on the internet, but I doesn't know where anymore).
When all appdomains are retrieved we get the single one which is the default (this is the IIS itself). With the default appdomain and the full path of the assembly, which contains the CefSharpRenderer, we can create an instance of the Renderer in the context of the default appdomain.
Then the call is forwarded to this instance.

public class CefSharpRendererProxy : ICefSharpRenderer
{
    public void RenderSomething()
    {
        //Get the default appdomain. This will also work if the default appdomain comes from a service like the IIS
        var defaultAppDomain = GetAppDomains().Single(domain => domain.IsDefaultAppDomain());
        //Get the path to the assembly where the CefSharpRenderer is implemented
        var pathToAssembly = new Uri(Assembly.GetAssembly(typeof(CefSharpRenderer)).CodeBase).LocalPath;

        //Create a new instance of the CefSharpRenderer in the context of the default appdomain
        var instance = (ICefSharpRenderer)defaultAppDomain.CreateInstanceFromAndUnwrap(pathToAssembly, typeof(CefSharpRenderer).FullName);
        instance.RenderSomething();
    }

    private static List<System.AppDomain> GetAppDomains()
    {
        var appDomains = new List<System.AppDomain>();
        var enumHandle = IntPtr.Zero;
        var host = new CorRuntimeHostClass();
        try
        {
            host.EnumDomains(out enumHandle);

            while (true)
            {
                object domain;
                host.NextDomain(enumHandle, out domain);
                if (domain == null) break;
                var appDomain = (System.AppDomain)domain;
                appDomains.Add(appDomain);
            }
            return appDomains;
        }
        catch (Exception)
        {
            return null;
        }
        finally
        {
            host.CloseEnum(enumHandle);
            Marshal.ReleaseComObject(host);
        }
    }
}

In it's final version the CefSharpRendererProxy could check whether the current appdomain is the default one and then instanciate the Renderer in the common way. But this is only an idea.

The CefSharpRenderer must inherit the MarshalByRefObject because otherwise calls between the current appdomain and the default appdomain cannot be serialized. This would throw an exception.

I will attach a simple example in which a console application creates a new appdomain and tries to interact with CefSharp. Also a WebApplication is inside this example. It shows that also with the IIS this approach works like a charm. For use this approach in a WebApplication the ShadowCopying must be disabled in the web.config otherwise Cef doesn't find it's references, but this is only a little problem.

I hope this is understandable. Feel free to ask me any questions.

CefSharp.AppDomain.zip

@amaitland
Copy link
Member

@flole Great you have something working. In future, please refrain from discussing your ideas in a Pull Request. The relevant open issue is linked above. #351

I will attach a simple example

As per Contributing.md, please no zip files.

https://github.com/cefsharp/CefSharp/blob/master/CONTRIBUTING.md#help-us-help-you

@flole
Copy link

flole commented Aug 27, 2016

@amaitland Sorry, I will post my workaround again under the issue #351 and create a new repo for my sample code.

@vradchuk-cgn
Copy link

@arsher, thanks for the great feature! Unfortunately I cannot compile this branch against VS'15. Did you experience this issue?
Appreciate your effort, if you could share NuGet package of this branch.

@wobbince
Copy link

Any update on this or when it will be merged?
I'm having trouble building this in Visual Studio 2015 for use in a VSTO.

@whichwit
Copy link

whichwit commented Mar 2, 2017

Express interest as well although I do not have sufficient knowledge for the underlying library build and linking. Trying to use CefSharp embedded in an UserControl to host in another application. Having Chrome > IE many times over.

@amaitland
Copy link
Member

This PR has been open for over a year and nobody has put their hand up to take over. It's not something I require and am not prepared to spend my own time implementing this. The maintenance burden is too high.

There are two other actively developed CEF wrappers

https://bitbucket.org/xilium/xilium.cefglue/overview
https://bitbucket.org/chromiumfx/chromiumfx/overview

@amaitland amaitland closed this Mar 5, 2017
@rajamahalingam
Copy link

rajamahalingam commented Nov 21, 2017

Hi,
I tried the below steps to Making CefSharp compatible with non-default AppDomains based on this articles but It seems 3 files are missing(incomplete reference code) to complete this implementation. Please find and verify the below steps which is followed based on this articles. Kindly help me to complete this implementation.

  1. Created prebuild project to that can generate AppDomain safe wrappers.. - This is working fine
  2. Added Pre-Build event to CefSharp.Core to run the Prebuild executable - This is working fine
  3. Small modifications in CefSharp.Core to make use of generated wrappers - For this change three header files are missing and Am not able to find anywhere 1.) Safe/CefAppSafe.h 2.)Safe/CefWebPluginInfoVisitorSafe.h 3.)Safe/CefSchemeHandlerFactorySafe.h. Can anyone help me on this.

@hiteshmaniya
Copy link

This week I ran into the same problem with the GCHandel and multiple AppDomains. In my case I want to render an html file which contains a d3 chart in an ASP.NET WebApi. Because the default appdomain is owned by the IIS itself I couldn't do that.

After I looked at the code from @arsher I saw that the unmanaged part of CefSharp talks only with the default appdomain, which is in my case the IIS. My workaround for this is easy, let me explain:

I have a class called CefSharpRenderer which looks like this. What it does is simple. It initializes Cef when it isn't already, then it creates a new ChromiumWebBrowser and attach some events. When the browser is initialized Google is loaded. For my workaround it is necessary that this class inherits the MarshalByRefObject (why comes later). Also this class implements the method void RenderSomething(); which is defined by the ICefSharpRenderer.

public class CefSharpRenderer : MarshalByRefObject, ICefSharpRenderer
{
    private ChromiumWebBrowser _browser;
    private SemaphoreSlim _renderingFinishedSemaphore = new SemaphoreSlim(0, 1);
    public void RenderSomething()
    {
        if (!Cef.IsInitialized)
        {
            var settings = new CefSettings();

            var osVersion = Environment.OSVersion;
            //Disable GPU for Windows 7
            if (osVersion.Version.Major == 6 && osVersion.Version.Minor == 1)
            {
                // Disable GPU in WPF and Offscreen examples until #1634 has been resolved
                settings.CefCommandLineArgs.Add("disable-gpu", "1");
            }

            //Perform dependency check to make sure all relevant resources are in our output directory.
            Cef.Initialize(settings, shutdownOnProcessExit: true, performDependencyCheck: false);
        }


        _browser = new ChromiumWebBrowser();
        _browser.BrowserInitialized += _browser_BrowserInitialized;
        _browser.LoadingStateChanged += _browser_LoadingStateChanged;

        _renderingFinishedSemaphore.Wait();
    }

    private void _browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
    {
        if (e.IsLoading)
        {
            return;
        }

        //Google has been loaded
        //Yay!
        _renderingFinishedSemaphore.Release();
    }

    private void _browser_BrowserInitialized(object sender, EventArgs e)
    {
        _browser.Load("http://www.google.de");
    }
}

To make my workaround transparent to the caller I have an additional class which also implements the ICefSharpRenderer interface. This class is called CefSharpRendererProxy and contains the main work. When the RenderSomething() method is called it retrieves all appdomains from the current process with the GetAppDomains method (I found this method on the internet, but I doesn't know where anymore).
When all appdomains are retrieved we get the single one which is the default (this is the IIS itself). With the default appdomain and the full path of the assembly, which contains the CefSharpRenderer, we can create an instance of the Renderer in the context of the default appdomain.
Then the call is forwarded to this instance.

public class CefSharpRendererProxy : ICefSharpRenderer
{
    public void RenderSomething()
    {
        //Get the default appdomain. This will also work if the default appdomain comes from a service like the IIS
        var defaultAppDomain = GetAppDomains().Single(domain => domain.IsDefaultAppDomain());
        //Get the path to the assembly where the CefSharpRenderer is implemented
        var pathToAssembly = new Uri(Assembly.GetAssembly(typeof(CefSharpRenderer)).CodeBase).LocalPath;

        //Create a new instance of the CefSharpRenderer in the context of the default appdomain
        var instance = (ICefSharpRenderer)defaultAppDomain.CreateInstanceFromAndUnwrap(pathToAssembly, typeof(CefSharpRenderer).FullName);
        instance.RenderSomething();
    }

    private static List<System.AppDomain> GetAppDomains()
    {
        var appDomains = new List<System.AppDomain>();
        var enumHandle = IntPtr.Zero;
        var host = new CorRuntimeHostClass();
        try
        {
            host.EnumDomains(out enumHandle);

            while (true)
            {
                object domain;
                host.NextDomain(enumHandle, out domain);
                if (domain == null) break;
                var appDomain = (System.AppDomain)domain;
                appDomains.Add(appDomain);
            }
            return appDomains;
        }
        catch (Exception)
        {
            return null;
        }
        finally
        {
            host.CloseEnum(enumHandle);
            Marshal.ReleaseComObject(host);
        }
    }
}

In it's final version the CefSharpRendererProxy could check whether the current appdomain is the default one and then instanciate the Renderer in the common way. But this is only an idea.

The CefSharpRenderer must inherit the MarshalByRefObject because otherwise calls between the current appdomain and the default appdomain cannot be serialized. This would throw an exception.

I will attach a simple example in which a console application creates a new appdomain and tries to interact with CefSharp. Also a WebApplication is inside this example. It shows that also with the IIS this approach works like a charm. For use this approach in a WebApplication the ShadowCopying must be disabled in the web.config otherwise Cef doesn't find it's references, but this is only a little problem.

I hope this is understandable. Feel free to ask me any questions.

CefSharp.AppDomain.zip

I have downloaded this solution and it is working with console app domain, but your WebApplication is showing only loading screen only. When I have traced it with performDependencyCheck=true then it gives error of

Server Error in '/' Application.
Unable to locate required Cef/CefSharp dependencies:
Missing:CefSharp.BrowserSubprocess.exe
Missing:CefSharp.BrowserSubprocess.Core.dll
Missing:CefSharp.Core.dll
Missing:CefSharp.dll
Missing:icudtl.dat
Missing:libcef.dll
Executing Assembly Path:C:\Users\x\Downloads\CefSharp.AppDomain\WebApplication\bin
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.Exception: Unable to locate required Cef/CefSharp dependencies:
Missing:CefSharp.BrowserSubprocess.exe
Missing:CefSharp.BrowserSubprocess.Core.dll
Missing:CefSharp.Core.dll
Missing:CefSharp.dll
Missing:icudtl.dat
Missing:libcef.dll
Executing Assembly Path:C:\Users\x\Downloads\CefSharp.AppDomain\WebApplication\bin

Also If I run with performDependencyCheck=false then I am getting Cef.IsInitialized as true.

@Dileepreddyj
Copy link

Hi @arsher, I know it has been long since you gave this workaround.
Any idea if a full time fix is available for this issue in cefsharp?
Wanted to try these on the latest cefsharp version and see a lot of changes since you originally provided these code changes.
Also I do not see any of the files you mentioned in the code from safe/*, any idea where we can pick those files from?
Example:
#include "Safe/CefAppSafe.h"
#include "Safe/CefWebPluginInfoVisitorSafe.h"
#include "Safe/CefSchemeHandlerFactorySafe.h"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.