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

Multiple AppDomains error "Cannot pass a GCHandle across AppDomains". #351

Closed
brock8503 opened this issue May 8, 2014 · 24 comments
Closed

Comments

@brock8503
Copy link
Contributor

Hey team,

I recently stumbled upon this pretty nasty gotcha when trying to host our browser control inside a plugin system that loads a new AppDomains per plugin. When the native libcef.dll makes a call back into managed there is an exception thrown "Cannot pass a GCHandle across AppDomains".

I found that there have been several post written about this problem-
http://lambert.geek.nz/2007/05/29/unmanaged-appdomain-callback/
http://www.lenholgate.com/blog/2009/07/error-cannot-pass-a-gchandle-across-appdomains.html

Highlight from these posts-
"Consequently, when calling managed code from unmanaged code, the compiler has to pick one AppDomain to use, and it appears to pick the first one."

To test this issue

  • Create a new WPF application
  • Change App.xaml Build Action to Page (right-click Properties->Build Action->Page)
  • Add file below with main - Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CefSharp;

namespace MultipleAppDomains
{
    class Startup
    {
        [STAThread()]
        static void Main()
        {
            AppDomain domain = AppDomain.CreateDomain("another domain");
            CrossAppDomainDelegate action = () =>
            {
                var settings = new CefSettings
                {
                    RemoteDebuggingPort = 8088,
                    BrowserSubprocessPath = "CefSharp.BrowserSubprocess.exe",
                    LogSeverity = LogSeverity.Verbose

                };

                if (!Cef.Initialize(settings))
                {
                    // Do Something
                }
                App app = new App();
                app.MainWindow = new MainWindow();
                app.MainWindow.Show();
                app.Run();
            };
            domain.DoCallBack(action);
        }
    }
}

I'd like to get the discussion going and come up with a good course of action.

@jornh jornh mentioned this issue May 25, 2014
@jornh
Copy link
Contributor

jornh commented May 25, 2014

@brock8503 I guess we have found another place where this now bites us: #366. So now we may have to pay more attention to your request for discussion 😜

In the time since you posted did you get any wiser on how to resolve or workarond this on your own?

@brock8503
Copy link
Contributor Author

@jornh Looks like you stumbled upon the exact error that a few others did with xunit. It was mentioned in the sources above.

In my mind we need to revisit libcef_wrapper and probably create a new native-manage wrapper with the thunk layer proposed in the above articles. Or for every callback we will need to add the thunk redesign in cefsharp.dll.

I am curious in studying how the c# interop marshaling handles this.

@jornh jornh added this to the 33.0.0 milestone Jun 8, 2014
@perlun perlun mentioned this issue Jun 10, 2014
@brock8503
Copy link
Contributor Author

@jornh @perlun Sorry for going dark the last month, lots of code to ship on our end. Did we make any progress on this issue or is it something we still are looking to fix?

@jornh
Copy link
Contributor

jornh commented Jun 24, 2014

@brock8503 no problem! Of course you need to work on what pays your bills 😄 Great to see you around again though!

If it makes you feel any better I don't think any one else had any progress on this one either. We are currently trying to see if we can polish things enough up to take off the -pre label. So it's still on the todo.

@herebebeasties
Copy link

See also #248 by @joaompneves for the CEF1 branch, which was deemed a little over-intrusive. Might be portable to CEF3?

@jornh jornh modified the milestones: 3000, 37.0.0 Nov 12, 2014
@msiddiqi
Copy link

msiddiqi commented Jan 2, 2015

I see the same issue :(

@stevozilik
Copy link

+1 We have the same issue trying to use the browser in Excel (DNA) plugin

@amaitland
Copy link
Member

As this is a non-trivial task I think it'll come down to someone scratching their own itch. If that's not appealing then the other option is to look at CefGlue. I don't know much about it personally, just that it uses PInvoke so shouldn't have the same issue as there's no c++ code.

https://bitbucket.org/xilium/xilium.cefglue/wiki/Home

@neilgallagher
Copy link

We also have this issue from an Excel Plugin. It seems that it was first noticed some years ago. Is there a plan to fix? Thanks

@amaitland
Copy link
Member

@neilgallagher See comment above.

@jornh
Copy link
Contributor

jornh commented Jan 26, 2015

Here's maybe another option for those needing to integrate as a add-in to MS Stuff like Office? It's made to work with SQL Server Management Studio (aka SSMS). I'm thinking the problem (and solution) is maybe the same across SSMS, maybe Visual Studio and Office? So take a peek at https://www.nuget.org/packages/RedGate.AppHost/

I don't know anything more about this at the moment - and I don't personally have this need. Just wanted to throw this out there for you guys. So by all means try to scratch you own itch on this if possible.

@ezexe
Copy link

ezexe commented Feb 14, 2015

+1 on this one any1 figure it out yet?

@rassilon
Copy link
Contributor

@herebebeasties , comment to use the approach in #248 appears to be the correct one from the .Net AppDomain single process perspective. The changes in #248 do beg the question about whether or not a creative macro might make this pattern easier to remember to use and apply. Otherwise, that's a ton of extra boilerplate just for this one issue.

If folks brain storm about an acceptable macro pattern here, maybe someone will do the grunt work.

Bill

ps @jornh, that RedGate host stuff looks awesome, I wish I'd seen that in the past. I could have used that on a project of mine instead of some from scratch code I wrote that probably isn't as nice.

@jepp
Copy link

jepp commented May 28, 2015

We're affected by this as well. We're trying to use RemObjects Hydra to allow a CefSharp-based control to be plugged into a Delphi app. Traced it back to this same error. Has there been any look at what rassilon mentioned? I'd do it, but I'm already pretty far out of my depth.

@amaitland
Copy link
Member

To be honest, even if someone did undertake the huge amount of work required, I'd be very reluctant to see the changes merged in, as just about every hook into CEF would need to be changed, which would require most testing/resources to troubleshoot/bug fix. I just don't see it being practical.

I think there are two practical options, switch to CefGlue or look at the RedGate host option.

@rassilon
Copy link
Contributor

How much use is JUST a FrameworkElement for driving some automated tests via RedGate's approach?

It looks mostly like a proof of concept you need to extend yourself. (Hopefully, I'm missing something..)

Bill

@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 pull request (#1556) 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.

A repo with an simple example is published here: CefSharp.AppDomain

In the example 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.

@amaitland
Copy link
Member

A repo with an simple example is published here: CefSharp.AppDomain

@flole Thanks for posting your example 👍 Have you tested this in a real world scenario? I'm curios as to how things behave once the Application Pool starts recycling Worker Processes?

@flole
Copy link

flole commented Sep 1, 2016

Yes, I currently use this to generate pdf data sheets. What I can say is that so far there are no problems. It just works.

I think if the Application Pool recycles it's worker processes, Cef is shutdown for this process. The other worker processes should not have problems with it. Their Cef keeps initialized and the Browser Subprocess still works.

@stever
Copy link

stever commented Sep 28, 2016

@jornh Thanks for that tip. I forgot all about this issue and went looking for a new version of CefSharp to use with Excel-DNA, and rediscovered the issues here. I had been using the changes that @arsher had posted in #1556 previously, which was good enough. Still, I thought I'd give RedGate.AppHost a try.

It works! Here's a working example using the current NuGet release package.

[Edited to add some further info:] Trouble with RedGate.AppHost is the one-way communication. In order to call Cef.Shutdown() and thereby ensure that cookies are persisted, I've added code to poll the server until it picks up that the window has been closed. Not sure if there's a better way to do this.

@jn-sudeep
Copy link

@flole, We trying to use CefSharp control on a Windows form and encountered cross domain issue. I am tyring @flole solution. You have written this example for web based scenario, how I can use this solution on a Windows form where we need to add CefSharp browser control inside a parent control like Panel?

@perlun
Copy link
Member

perlun commented Dec 2, 2017

It seems like we have workarounds in place for running in separate appdomains. Thanks and 👍 to everyone for your valuable input! I will close this now, it doesn't make sense to have issues open for years. 😃

If anyone wants to improve on this further, just submit a PR as usual.

@dharmeshtailor
Copy link

@flole your code worked for me. We wanted to show the browser under dialog and worked like charm. Thanks very much.

@amaitland
Copy link
Member

It works! Here's a working example using the current NuGet release package.

[Edited to add some further info:] Trouble with RedGate.AppHost is the one-way communication. In order to call Cef.Shutdown() and thereby ensure that cookies are persisted, I've added code to poll the server until it picks up that the window has been closed. Not sure if there's a better way to do this.

For anyone interested I'm working on a CefSharp specific out of process implementation at https://github.com/cefsharp/CefSharp.OutOfProcess

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

No branches or pull requests