Making CefSharp compatible with non-default AppDomains by arsher · Pull Request #1556 · cefsharp/CefSharp
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.
@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.
This is really great news. I'll try to build this on myself while it makes its way to the master :-)
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.
@arsher, what kind of help you need for this to make to master?
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
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.
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.
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.
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.
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.
@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.LibclangSharppackage? 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 maturelibclangwrapper be chosen? Or another option?
The practical aspects:
- Merge in current
masterand resolve any merge conflicts - Fix the naming,
Safedoesn'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
libclangwrapper). - Add extensive unit tests
- Update the
CIenvironment 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),
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?
@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.
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.
@amaitland Sorry, I will post my workaround again under the issue #351 and create a new repo for my sample code.
flole
mentioned this pull request
@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.
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.
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.
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.
- Created prebuild project to that can generate AppDomain safe wrappers.. - This is working fine
- Added Pre-Build event to CefSharp.Core to run the Prebuild executable - This is working fine
- 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.
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
ChromiumWebBrowserand attach some events. When the browser is initialized Google is loaded. For my workaround it is necessary that this class inherits theMarshalByRefObject(why comes later). Also this class implements the methodvoid RenderSomething();which is defined by theICefSharpRenderer.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
ICefSharpRendererinterface. This class is calledCefSharpRendererProxyand contains the main work. When theRenderSomething()method is called it retrieves all appdomains from the current process with theGetAppDomainsmethod (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 theCefSharpRenderer, 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
CefSharpRendererProxycould 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
CefSharpRenderermust inherit theMarshalByRefObjectbecause 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.
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.
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"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters