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

Type Safe Marshaling of Objects Between .NET and Javascript #3138

Closed
wants to merge 5 commits into from

Conversation

andrewmd5
Copy link
Contributor

@andrewmd5 andrewmd5 commented May 24, 2020

Fixes:
#3137 #3129 #2231 and probably a whole host of people who have been surprised to find out that BindingOptions.CamelCaseJavascriptNames is a one way operation.

Summary:
When CefSharpSettings.TypeSafeMarshaling is set to true CefSharp will use strict type-safe analysis for sending and receiving data.

  • Javascript objects can be assigned to their corresponding .NET type, even if it is a Guid or Tuple.
  • Nulls for non-nullable types and other undefine behavior raise detailed exceptions to developers so they can fix their code.
  • .NET types like Guid and Tuple are valid return types for .NET methods returning to Javascript.
  • Added support for asynchronous Task.
  • Security has been introduced to prevent immutable types from being marshaled.
  • .NET code can keep it's .NET coding conventions and Javascript and TypeScript code can keep theirs.

For extra "safety" I'll open a separate pull request for a TypeScript code generator.

Changes:

Because our build of CefSharp targets .NET 4.7.2 we have the luxury of some modern .NET features. For this change I had to implement things like a crude Tuple and ValueTuple (which isn't even supported in 4.5.2, and is no longer supported by Microsoft) detection, but it seems to work fine.

Other than that, we applied only the code that could be introduced without making breaking changes. This pull request has backwards combability for people who built their code around the default binder, but when they enable the new setting their code will probably break.

Which probably isn't entirely a bad thing because using this code in production, we've eliminated almost all issues you would expect to get from sending complex objects from a typed language, to an untyped language, and back again.

How Has This Been Tested?

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Updated documentation

Checklist:

  • Tested the code(if applicable)
  • Commented my code
  • Changed the documentation(if applicable)
  • [] New files have a license disclaimer
  • The formatting is consistent with the project (project supports .editorconfig)

Andrew Sampson added 2 commits May 24, 2020 03:35
- Adds a type safe binder that can reliably turn Javascript objects back into .NET types without forcing the .NET and Javascript code to break from their standard conventions. It also has better support for Enums, .NET only types, and catches undefined behavior
- Adds a type safe interceptor that will handle serializing the return values of  .NET methods (both synchronous and asynchronous) to Javascript compatible values. That includes .NET only types like Guid and Tuple.
- Adds a new TypeBindingException that will allow developers to catch errors in their code such as nulls or data type mismatches.
- Adds async execution support for IMethodInterceptor
@AppVeyorBot
Copy link

@AppVeyorBot
Copy link

@AppVeyorBot
Copy link

@andrewmd5
Copy link
Contributor Author

@amaitland if I can make one suggestion -- .NET 4.5.2 is no longer supported by Microsoft and was released in 2013. Windows 7 is no longer supported by Microsoft. CefSharp should bump it's target to 4.7.2 because perpetual backwards compatibility means not being able to take advantage of what the language has to offer today.

@amaitland
Copy link
Member

@Codeusa Thanks, looks interesting 👍 I'll have a look and respond in detail hopefully tomorrow.

Copy link
Member

@amaitland amaitland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes:
#3129

From what I can tell your IBinder implementation would throw an exception as detailed in #3129 You can run it through the unit test at ef59044#diff-d5a2c335de34e13e7d04a3681643f9ccR125 if you are interested.

Screenshots (if appropriate):

Looks like you have some test code or unit tests, are you able to provide the code to use as a basis for some unit tests?

  • Nulls for non-nullable types and other undefine behavior raise detailed exceptions to developers so they can fix their code.

Null for value types is a tricky one, it's kind of an opinionated topic, I'm actually in favour of assigning the default value for a type. e.g. null for an integer maps to 0. This is the current behaviour for the 81.3.100 release. Your TypeSafeBinder implementation is largely copied from the DefaultBinder which previously only converted null to the default value when part of a collection reference https://github.com/cefsharp/CefSharp/pull/3138/files#diff-68f2409c5b26e3887e39a37dcf038496R131

  • Security has been introduced to prevent immutable types from being marshaled.

Seems reasonable, thanks 👍 We should update the DefaultBinder, probably use a common base class (possibly your implementation can replace the default implementation, I need to compare and determine impact on existing users).

For extra "safety" I'll open a separate pull request for a TypeScript code generator.

What dependencies does it have exactly? Probably needs to be in it's own class library and published as a separate Nuget package if it's going to have additional dependencies.

Because our build of CefSharp targets .NET 4.7.2 we have the luxury of some modern .NET features

If the code would benefit greatly from a newer .Net version then we should look at a separate class library and publishing a different Nuget package, can probably go with the TypeScript generator.

Other than that, we applied only the code that could be introduced without making breaking changes

Unfortunately there are breaking changes, you've added new methods to the public API, the javascript naming is now potentially different. The current camelcase conversion is basic and yours looks like an excellent improvement, there is however scope for the generation of a different name after upgrading to a version that includes this code.

Which probably isn't entirely a bad thing because using this code in production, we've eliminated almost all issues you would expect to get from sending complex objects from a typed language, to an untyped language, and back again.

Your binder implementation looks like it was based off the DefaultBinder, I'm hopeful we can at a minimum use a common base class to reduce code duplication.

  • The formatting is consistent with the project (project supports .editorconfig)

To be consistent the current code uses braces for even single line if/case blocks. I'm happy to fix this up after merge.

.NET 4.5.2 is no longer supported by Microsoft

Do you have an official Microsoft reference that states it's no longer supported?

Windows 7 is no longer supported by Microsoft.

It's my understanding that .Net 4.5.2 is tied to the lifespan of Windows 8.1. The .Net Product Lifecycle has .Net 4.5.2. listed as a OS component, Windows 8.1 shipped with .Net 4.5.1 as per https://docs.microsoft.com/en-us/dotnet/framework/get-started/system-requirements#supported-client-operating-systems which was replaced with .Net 4.5.2 as the supported OS Component. A quick check and others have a similar opinion see https://stackoverflow.com/questions/58732338/when-is-end-of-life-for-dotnet-framework-4-5-2

If you've got something from MS that states otherwise then I'd be very interested in a link.

CefSharp should bump it's target to 4.7.2 because perpetual backwards compatibility means not being able to take advantage of what the language has to offer today.

If it was just up to my personal preference then I'd be all in favour of updating. Have to consider the existing user base, a large percentage of developers have set version requirements and cannot upgrade to a newer version. I have previously stated that I'd stick with the Minimum supported version of .Net. I still get the occasional request for support for .Net 4.0 and .Net 4.5.

There are actually people backporting CefSharp to older versions of .Net e.g. https://github.com/Secbyte/CefSharp.Net40

/// This setting has no backwards compatibility with the default binding behavior, so if you built code around it enabling this will probably break app. <br/> <br/>
/// All bound objects and CefSharp post will use a internal TypeSafe binder and interceptor to passively handle objects.
/// </summary>
public static bool TypeSafeMarshaling { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using this sort of global static property for things related to javascript binding was a mistake my part, it's difficult to test and not very flexible. I think the cleanest and simplest way forward is to add a static property or method to BindingOptions that creates the binder and interceptor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this is the best approach, I've never been a fan of massive configuration files since it increases the surface area of your conditional checking.

@@ -190,15 +191,17 @@ public void Register(string name, object value, bool isAsync, BindingOptions opt
throw new ArgumentException("Registering of .Net framework built in types is not supported, " +
"create your own Object and proxy the calls if you need to access a Window/Form/Control.", "value");
}

var camelCaseJavascriptNames = options == null ? true : options.CamelCaseJavascriptNames;
// if TypeSafeMarshaling is enabled then we always set names to camelCase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer not to make any changes in here other than improving the property naming.

  • Testing becomes more difficult
  • Easily get users who've enabled the setting and registered the object as sync wondering why the code isn't behaving. I understand it's documented, there will be more than a few who won't read it.

@@ -314,6 +317,14 @@ internal bool TryCallMethod(long objectId, string name, object[] parameters, out
}
else
{
// anyone extending the Method Interceptor should be able to use Task
if (obj.MethodInterceptor.IsAsynchronous)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really keen on this either, in this case specifically the code would just as easily be in the TypeSafeInterceptor implementation without having to make any breaking changes to the API.

We could potentially split the async handling out into

  • A decorator that holds all the async specific functionality.
  • A base class that's that has some protected virtual methods to override

We can then test that functionality in isolation and allow users to leverage that code in their custom implementation.

Using Castle.DynamicProxy as a model, I think we keep the interceptor as is and rely on the implementation to deal with the async nature. Reference https://github.com/castleproject/Core/blob/master/docs/dynamicproxy-async-interception.md

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Codeusa I'll make my proposed code changes and push them to a branch for further discussion. Hopefully in the next day or so I'll have something to discuss 👍

Thanks for the review! I will look over your comments in the next few hours and reply. I also apologize if any of my comments inline came off as opinionated (bumping version, etc.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

. I also apologize if any of my comments inline came off as opinionated (bumping version, etc.)

Totally fine 😄 Hopefully my response came across as diplomatic, personally I wish bumping version was a simple process that everyone would be onboard with. When I upgraded from .Net 4.0 to .Net 4.5.2 i copped quite a bit of flack from people. I'm hoping to avoid comments like #1785 (comment) if can be helped 😦

When I can find a few minutes I'll try install the Microsoft.Net.Compilers packages again, at least we'll be able to use the newer syntax if I can get it building.

/// <summary>
/// Returns an enumerable sequence of bindable properties for the specified type.
/// Returns an enumerable sequence of bindable properties for the specified type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like some unnecessary whitespace has been added.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, my code styling was applied for our main project and we use indented XML comments.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use Edit -> Advanced -> Format Document it should apply the editorconfig style.

Personally I use https://marketplace.visualstudio.com/items?itemName=mynkow.FormatdocumentonSave

return nativeType.CreateEnumMember(javaScriptObject);
}

// if the source object is null and is not an enum, then there isn't anything left to do.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation as of 83.1.100 will map null to the default value for a Value Type. I've had requests for this behaviour previously. See ef59044#diff-e2e8176d85173d54b9b3dec2acadac86R32

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably make this a configurable option then. For example in our production use we ran into a bug and didn't realize the Javascript object was null because it was using the default of the Enum. There is some value in strict null checks from a development perspective.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, somehow I totally missed this set of comments.

Configurable is fine with me. I can see people wanting to use either option.

/// <param name="javaScriptObject">An instance of the Javascript object.</param>
/// <param name="nativeType">The type this method will try to create an instance of using the Javascript object.</param>
/// <returns>An instance of the native type.</returns>
public object Bind(object javaScriptObject, Type nativeType)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think nativeType is a little ambiguous, what's the type native to? The interface param is targetParamType, I'd be happy with something else, modelType maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well model is usually referring to a class of some sort, whereas this method can be used to bind primitives as well, so maybe destinationType is best?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

destinationType is fine with me 👍 Happy to update the interface and default implementation to match.

var javaScriptType = javaScriptObject.GetType();

// if the object can be directly assigned to the destination type, then return and let the runtime handle the rest.
if (nativeType.IsAssignableFrom(javaScriptType))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From here on this method is pretty much identical to the DefaultBinder, to reduce code duplicate we should probably have a common base class if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely, I based it around DefaultBinder as not to introduce a few dozen files into this pull. I also left out generic type resoling (e.g. Model) because it would have been a lot for one pull.

if (javaScriptItem == null)
{
// for value types like int we'll create the default value and assign that as we cannot assign null
model.Add(genericType.IsValueType ? Activator.CreateInstance(genericType) : null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DefaultBinder was updated to remove this block of code, we just need to add Value Type checking for null to the Bind method and none of this is required. See ef59044#diff-e2e8176d85173d54b9b3dec2acadac86L109

/// <returns>
/// An instance of the .NET type which should have all the same values as the <paramref name="javaScriptObject"/>
/// </returns>
protected virtual object BindObject(Type nativeType, Type javaScriptType, object javaScriptObject)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Common base class seems reasonable, on the surface I'm happy to adopt these changes into the DefaultBinder.

@amaitland
Copy link
Member

@Codeusa I'll make my proposed code changes and push them to a branch for further discussion. Hopefully in the next day or so I'll have something to discuss 👍

@amaitland
Copy link
Member

Work in progress of my proposed changes is at https://github.com/amaitland/CefSharp/commits/enhancement/binder

The code compiles but probably doesn't run yet, I've got a bit more to do, this is more of a status update.

I'll post a detailed follow up tomorrow after I have a chance to finish a few more things.

@amaitland
Copy link
Member

I've added BindingOptions.NameConverter in commit 898ca38 (master branch) which extracts the JavascriptObjectRepository's naming conversion into a pluggable and easily testable option.

@Codeusa Before I go manually importing the new binder/interceptor what do you think about having them in a separate library? Say something like CefSharp.TypeScript, you mentioned having some code for a TypeScript generator, they could live together? We can target a newer .Net version. Open to suggestions on naming for the library.

@andrewmd5
Copy link
Contributor Author

Hey @amaitland I apologize for the delay, I spent most of this week "offline" trying to support my local community during a unprecedented time. I will look over this today and provide feedback / suggestions where needed, an I appreciate your patience.

@amaitland
Copy link
Member

I apologize for the delay, I spent most of this week "offline" trying to support my local community during a unprecedented time.

@Codeusa Absolutely no need to apologise. Supporting your community is far more important than writing/reviewing code. Please take whatever time you need.

@andrewmd5
Copy link
Contributor Author

I've added BindingOptions.NameConverter in commit 898ca38 (master branch) which extracts the JavascriptObjectRepository's naming conversion into a pluggable and easily testable option.

@Codeusa Before I go manually importing the new binder/interceptor what do you think about having them in a separate library? Say something like CefSharp.TypeScript, you mentioned having some code for a TypeScript generator, they could live together? We can target a newer .Net version. Open to suggestions on naming for the library.

The Typescript code generator is just a single class (not counting reflection extensions) with no dependencies. I am going to clean it up before submitting. That being said, having the interceptor and binder in a library targeting a higher framework would be great! We could more easily support newer .NET types.

@amaitland
Copy link
Member

That being said, having the interceptor and binder in a library targeting a higher framework would be great!

@Codeusa Let's go with that plan then. Now the fun part, naming the library! 😄

Do we go with something generic with the possibility of having other unrelated functionality added later or keep it specific to binding and focus on say TypeScript?

  • CefSharp.TypeScript
  • CefSharp.Extensions
  • ???

Open to suggestions.

@andrewmd5
Copy link
Contributor Author

That being said, having the interceptor and binder in a library targeting a higher framework would be great!

@Codeusa Let's go with that plan then. Now the fun part, naming the library! 😄

Do we go with something generic with the possibility of having other unrelated functionality added later or keep it specific to binding and focus on say TypeScript?

  • CefSharp.TypeScript
  • CefSharp.Extensions
  • ???

Open to suggestions.

Again sorry to open this pull only to become the most inactive person in the world.

CefSharp.Extensions is probably the most accurate name available. It will allow for the adding of additional extension features without distorting the purpose of the library. It can also be used as a place to experiment with new features without bumping the main CefSharp assembly.

@andrewmd5
Copy link
Contributor Author

andrewmd5 commented Jun 10, 2020

I've noticed a few issues in the logic around tuples so I will commit a patch for that and address some other changes you requested.

@amaitland
Copy link
Member

Again sorry to open this pull only to become the most inactive person in the world.

Totally fine. I myself have been sick and my 2yr old was sick before that (just a cold, nothing major), so haven't spent any time in front of a computer.

I've noticed a few issues in the logic around tuples so I will commit a patch for that and address some other changes you requested.

Do you know what the minimum .Net version is that supports the features you require?

@amaitland
Copy link
Member

I've added BindingOptions.NameConverter in commit 898ca38 (master branch) which extracts the JavascriptObjectRepository's naming conversion into a pluggable and easily testable option.

Just as a follow up to this I've created #3141 which moves the NameConverter to a property on the IJavascriptObjectRepository. This allows for

  • Naming of objects without use of an interceptor.
  • Objects passed to a IJavascriptCallback will also be named correctly.

Still a little bit to do before it's ready, default binder needs updating so it's naming matches. Bit of code cleanup and some additional test cases.

- Now only supports ValueTuples
- Improved the check for the type
- Added a binder that can successfully create a generic ValueTuple object when not running under a newer version of .NET
@andrewmd5
Copy link
Contributor Author

Again sorry to open this pull only to become the most inactive person in the world.

Totally fine. I myself have been sick and my 2yr old was sick before that (just a cold, nothing major), so haven't spent any time in front of a computer.

I've noticed a few issues in the logic around tuples so I will commit a patch for that and address some other changes you requested.

Do you know what the minimum .Net version is that supports the features you require?

So as you can see in 3ad514a, because CefSharp currently doesn't use a framework target that supports ValueTuples there is a lot of type punning.

Targeting 4.7.2 in the new library will allow for the use of C# 8, and make a lot of the reflection code cleaner overall.

@amaitland
Copy link
Member

Targeting 4.7.2 in the new library will allow for the use of C# 8, and make a lot of the reflection code cleaner overall.

To be included in the main repository we still need to be able to build using VS2015, according to https://www.appveyor.com/docs/windows-images-software/#net-framework the build workers only have .Net 4.6.2 installed as the highest version on the VS2015 image.

We have a couple of options

  1. Target .Net 4.6.2 and use https://www.nuget.org/packages/System.ValueTuple/ to support tuple. We can use https://www.nuget.org/packages/Microsoft.Net.Compilers/2.10.0 which gives newer .Net compiler, not sure exactly which version, if I had to guess based on release date it's say 7.3.
  2. Create a new project under the CefSharp organisation on GitHub and use .Net 4.7.2

I'm leaning towards option two, option suggestions though.

/// to.
/// </param>
/// <param name="javaScriptObject">A collection that contains all the components of the tuple.</param>
/// <returns>A tuple I'd fucking hope</returns>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the profanity please 😄

/// The custom parser below supports converting strings into a <see cref="Enum"/> that has the <see cref="FlagsAttribute"/> defined.
/// These are commonly used separators to assist that process.
/// </summary>
private static readonly char[] EnumSeparators = { '|', ',', ';', '+', ' ' };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a reference source for this list? Are you using different separators in your code to represent a set of flags?

Do we really need to support all these possibilities?

@andrewmd5
Copy link
Contributor Author

2. Create a new project under the CefSharp organisation on GitHub and use .Net 4.7.2

Let's create a new project under the org and use .NET 4.7.2, and then afterwards I can roll these changes into the new Extensions library rather than here. How are you thinking about the API design for applying the extensions?

@amaitland
Copy link
Member

Let's create a new project under the org and use .NET 4.7.2, and then afterwards I can roll these changes into the new Extensions library rather than here.

We can use the new SDK project format and build a specific .Net Core version as well for minimal cost. I'll create a new repository after I've finished the official 83.4.x release 😄

How are you thinking about the API design for applying the extensions?

Keep it simple to start with, a new interceptor and binder implementation that can manually be assigned. I was wondering if having a different binder/interceptor for each object was actually necessary, switching to a single instance per JavascriptObjectRepository (per ChromiumWebBrowser) would make things much simpler. Could then have an extension method that assigned the newer versions. Obviously a breaking change, I'm not sure if anyone actually uses a different binder/interceptor per object.

Open to suggestions

@amaitland
Copy link
Member

I've created https://github.com/cefsharp/CefSharp.Extensions and setup a basic test project so we can get some xUnit tests happening. I'll port this code over hopefully tomorrow.

amaitland added a commit to cefsharp/CefSharp.Extensions that referenced this pull request Jul 10, 2020
amaitland added a commit to cefsharp/CefSharp.Extensions that referenced this pull request Jul 10, 2020
@amaitland
Copy link
Member

This has been merged into https://github.com/cefsharp/CefSharp.Extensions/tree/master/CefSharp.Extensions/ModelBinding with some changes

  • TypeSafe naming replaced with Strict e.g. StrictModelBinder
  • Added StrictBindingOptions which defaults to using the new binder/interceptor
  • Added license to each file
  • Ported the current xUnit tests (with modifications).

The project is targeting .Net Standard 2.0, ValueTuple is supported via the Nuget. You should be able to use the latest C# syntax using VS2019.

Closing this PR now as the code lives in a different repository. By all means open new issues or submit PR's against https://github.com/cefsharp/CefSharp.Extensions

@amaitland amaitland closed this Jul 10, 2020
@andrewmd5
Copy link
Contributor Author

@amaitland thank you for moving the needle on this. is there a preferred email I could reach out to you at?

@amaitland

This comment has been minimized.

@amaitland
Copy link
Member

I've pushed CefSharp.Extensions.83.4.20-pre.nupkg to Nuget.org, it should hopefully appear in the next hour or so.

I've created some very basic release notes with a summary based on the comments above, see https://github.com/cefsharp/CefSharp.Extensions/releases/tag/v83.4.20-pre
Additions/Edits please let me know and I'll happy make modifications.

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

Successfully merging this pull request may close these issues.

3 participants