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
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.
@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 :stuck_out_tongue_winking_eye:
In the time since you posted did you get any wiser on how to resolve or workarond this on your own?
@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 @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?
@brock8503 no problem! Of course you need to work on what pays your bills :smile: 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.
See also #248 by @joaompneves for the CEF1 branch, which was deemed a little over-intrusive. Might be portable to CEF3?
I see the same issue :(
+1 We have the same issue trying to use the browser in Excel (DNA) plugin
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.
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
@neilgallagher See comment above.
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.
+1 on this one any1 figure it out yet?
@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.
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.
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.
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
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.
A repo with an simple example is published here: CefSharp.AppDomain
@flole Thanks for posting your example :+1: 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?
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.
@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.
@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?
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.
@flole your code worked for me. We wanted to show the browser under dialog and worked like charm. Thanks very much.
Most helpful comment
@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.