cefsharp / CefSharp

.NET (WPF and Windows Forms) bindings for the Chromium Embedded Framework
http://cefsharp.github.io/
Other
9.89k stars 2.92k forks source link

NullReferenceException at CefSharp.Internals.JavascriptCallbackRegistry.Register #1289

Closed matthew-a-thomas closed 9 years ago

matthew-a-thomas commented 9 years ago

Seems to happen anytime I .EvaluateScriptAsync() any JS that will return a DOM object.

Tool Version
CefSharp.OffScreen 43.0.0
Visual Studio 2012
C#/.Net 4.6
Build 64 bits

Exception details:

System.NullReferenceException was unhandled
  HResult=-2147467261
  Message=Object reference not set to an instance of an object.
  Source=CefSharp.BrowserSubprocess.Core
  StackTrace:
       at CefSharp.Internals.JavascriptCallbackRegistry.Register(CefRefPtr<CefV8Context>* context, CefRefPtr<CefV8Value>* value)
       at CefSharp.Internals.Serialization.SerializeV8Object<class CefDictionaryValue,class CefStringBase<struct CefStringTraitsUTF16> >(CefRefPtr<CefV8Value>* obj, CefRefPtr<CefDictionaryValue>* list, CefStringBase<CefStringTraitsUTF16>* index, JavascriptCallbackRegistry callbackRegistry, deque<CefRefPtr<CefV8Value>\,std::allocator<CefRefPtr<CefV8Value> > >* seen)
       at CefSharp.Internals.Serialization.SerializeV8Object<class CefListValue,int>(CefRefPtr<CefV8Value>* obj, CefRefPtr<CefListValue>* list, Int32* index, JavascriptCallbackRegistry callbackRegistry, deque<CefRefPtr<CefV8Value>\,std::allocator<CefRefPtr<CefV8Value> > >* seen)
       at CefSharp.Internals.Serialization.SerializeV8Object<class CefListValue,int>(CefRefPtr<CefV8Value>* obj, CefRefPtr<CefListValue>* list, Int32* index, JavascriptCallbackRegistry callbackRegistry)
       at CefSharp.CefAppUnmanagedWrapper.OnProcessMessageReceived(CefAppUnmanagedWrapper* , CefRefPtr<CefBrowser>* browser, cef_process_id_t sourceProcessId, CefRefPtr<CefProcessMessage>* message)
       at CefExecuteProcess(CefMainArgs* , CefRefPtr<CefApp>* , Void* )
       at CefSharp.CefAppWrapper.Run()
       at CefSharp.BrowserSubprocess.Program.Main(String[] args)
  InnerException: 

It's Easy to Reproduce®:

Nuget config:

<packages>
  <package id="cef.redist.x64" version="3.2357.1287" targetFramework="net46" />
  <package id="cef.redist.x86" version="3.2357.1287" targetFramework="net46" />
  <package id="CefSharp.Common" version="43.0.0" targetFramework="net46" />
  <package id="CefSharp.OffScreen" version="43.0.0" targetFramework="net46" />
</packages>

C# demo

using (var browser = new ChromiumWebBrowser())
{
    using (var initializationGate = new ManualResetEvent(false))
    {
        browser.BrowserInitialized += new EventHandler((o, e) => // We need to know when it's initialized before moving on
            {
                Console.WriteLine("Initialized");
                initializationGate.Set();
            });
        if (browser.IsBrowserInitialized) // Silly fix for race condition
            initializationGate.Set();

        initializationGate.WaitOne(); // Wait until browser is initialized
    }

    browser.FrameLoadEnd += new EventHandler<FrameLoadEndEventArgs>((o, e) => // Prep callback
        {
            Console.WriteLine("Frame loaded " + e.Url + ". Press any key to break CefSharp . . .");
            Console.ReadKey(true);
            Console.WriteLine(" you pressed enter; executing JS...");

            browser.GetBrowser().MainFrame.EvaluateScriptAsync(
                "document.createElement('a')" // Nothing fancy. In Chrome console this will return a DOM object
            ).ContinueWith((response) =>
            {
                // Uh oh! This humble code will never run :'(
                Console.WriteLine("Finished executing JS. Result follows...");
                var result = response.Result;
                Console.WriteLine(result.Result.ToString());
            });
        });

    browser.Load("about:blank"); // Load about:blank, which when finished will execute our JS above

    // Sit and wait
    Thread.Sleep(int.MaxValue);
}

Output:

Initialized
Frame loaded about:blank. Press any key to break CefSharp . . .
 you pressed enter; executing JS...
[0924/215232:ERROR:browser_gpu_channel_host_factory.cc(134)] Failed to launch GPU process.
matthew-a-thomas commented 9 years ago

Sorry, I was a little vague. The exception isn't thrown from within managed code, so you don't get a nice breakpoint with locals and whatnot in Visual Studio, just a lovely Windows message that says "CefSharp.BrowserSubprocess has stopped working". Clicking "Debug the program" and choosing Visual Studio 2012 gives you the call stack.

amaitland commented 9 years ago

DOM objects are too complex for the serializer to handle.

https://github.com/cefsharp/CefSharp/wiki/Frequently-asked-questions#2-how-do-you-call-a-javascript-method-that-return-a-result

See the note on the FAQ entry.

matthew-a-thomas commented 9 years ago

@amaitland Thanks for linking to that FAQ; for some reason I never stumbled across it in all my Googling. Yet it says

there is no (easy) way to expose a random Javascript object to the .NET world, at least not today. However, one possible technique is to turn the Javascript object you wish to return to your .NET code into a JSON string with the Javascript JSON.toStringify() method and return that string to your .NET code. Then you can decode that string into a .NET object with something like JSON.net

I haven't yet looked closely at source code, but isn't something like the suggested workaround already being implemented? (I'm just observing that the type presented by EvaluateScriptAsync()'s response's Result is dynamic and does in fact expose random JavaScript objects). The core problem seems to already be at least solvable, so what does "too complex" mean?

I'm likely ignorant of some larger principle (I'm very new to CEF/CefSharp) that drove a design decision to impose this limitation. But in that case shouldn't this scenario be more gracefully handled? IMHO Separation of Concerns would suggest that code built on top of CefSharp shouldn't be responsible for knowing this boundary, or that this boundary should be exposed through an interface.

The FAQ references an MSDN JavaScript documentation page which mentions a "Circular reference in value argument not supported" exception, which IIRC is what one gets when one tries to serialize DOM objects in JavaScript. If that's the root issue then I'll look into adding a custom toJSON method to DOM prototypes and to whatever else is likely to break CefSharp. I'll try to report on how that goes within a couple of days, but I think this type of thing would only be a temporary workaround since one cannot possibly append a custom JavaScript serializer method to every JavaScript object that has a circular reference.

Thanks!

amaitland commented 9 years ago

I haven't yet looked closely at source code, but isn't something like the suggested workaround already being implemented?

What exactly do you mean already implemented?

I'm likely ignorant of some larger principle (I'm very new to CEF/CefSharp) that drove a design decision to impose this limitation. But in that case shouldn't this scenario be more gracefully handled? IMHO Separation of Concerns would suggest that code built on top of CefSharp shouldn't be responsible for knowing this boundary, or that this boundary should be exposed through an interface.

You raise a valid point and I'm all for improving the behavior. The relevant section of the API documentation is http://magpcss.org/ceforum/apidocs3/projects/%28default%29/CefV8Value.html

See also https://github.com/cefsharp/CefSharp/blob/cefsharp/43/CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.cpp#L176

From a technical point of view, if you only wish to solve the problem from a DOM point of view then the difficult part is determining if the returned object is a DOM element. Expanding the implementation to support any sort of object probably isn't that much harder. I'm open to suggestions/contributions.

From a philosophical point of I do believe it's important to remember this is an open source project with only a few active contributors. Polishing every last piece of the public facing API is time consuming and as this project isn't funded by a corporate interest, spending time on such things isn't always possible.

The FAQ references an MSDN JavaScript documentation page which mentions a "Circular reference in value argument not supported" exception

The MSDN reference was really just intended to provide information on the stringify function. (Probably should be a Chrome specific reference, though the MSDN one is still relevant in this context).

If that's the root issue then I'll look into adding a custom toJSON method to DOM prototypes and to whatever else is likely to break CefSharp.

Do you actually need to return the whole DOM node? Seems like a clunky design choice. Anyways, the basic problem is the circular reference, for which I'm sure there is a technical solution, it's really just a matter of someone investing the time. That section of code is C++ which seems to put off 99% people.

The native IPC exposed by CEF currently means you have to translate from a CefV8Value, to a CefListValue, then turn that into something meaningful in a .Net sense. You have to remember that Javascript is executed in a render process and needs to interact with the browser process.

The ChromiumFx project has taken an interesting approach to dealing with the problem, basically exposing the objects directly and crafting their own IPC layer similar to .Net remoting. At some point I plan to look into this further, see if anything is usable. I'd be curious to see how a chatty interface would actually perform in that scenario.

https://bitbucket.org/chromiumfx/chromiumfx

amaitland commented 9 years ago

Closing due to lack of feedback.