-
Notifications
You must be signed in to change notification settings - Fork 858
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
ConsString does not transforms to java.lang.String when passed to the host java object #247
Comments
Hi Guys. It is a super serious issue. It almost broke any previously working script that interacts with Java object instances. I have to downgrade to 1.7R3 to keep my code working. The only issue that I noted is that I can't pass Strings that was concatenated in JavaScript to my Java classes anymore. There is any workaround? I have thousands of little scripts that do that, and refactory all of them to call .toString() will cause to mutch trouble. |
Can you please provide a simple test case.
|
For better or worse, the ConsString class was introduced in 2011, and in
fact I recall that the same concept was used by the same developer at
Oracle when Nashorn was developed.
If someone wants to take the time to run benchmarks to see if it really
buys us anything performance-wise on a modern JVM, I'd be interested to
see. But it's been a long time since this was introduced and changing it
again may result in other problem...
…On Wed, May 1, 2019 at 1:10 PM RBRi ***@***.***> wrote:
Am 1. Mai 2019 20:22:01 MESZ schrieb "Eduardo Frazão" <
***@***.***>:
>Hi Guys. It is a super serius issue. It almost broke any previously
>working scripting that interacts with Java object instances. I have to
>downgrade to 1.7R3 to keep my code working. The only issue that I noted
>is that I can't pass Strings that was concatenated in JavaScript to my
>Java classes anymore. There is any workaroung to it? I have thousands
>of little scripts that can do that, and refactory every each one to
>call .toString() will cause to mutch trouble.
>
>--
>You are receiving this because you are subscribed to this thread.
>Reply to this email directly or view it on GitHub:
>#247 (comment)
Can you please provide a simple test case.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#247 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAD7I2ZUAE4PP2QPKLMR6N3PTH2MDANCNFSM4BQ5K27Q>
.
|
Sure. I will do that today. Thanks for answer! |
There are some performance measures available in #373. |
BTW still waiting for the sample |
|
any update on this issue? |
I'm not able to reproduce this, and don't see that a test case was ever included when requested. This is the oldest version of Rhino I have. I ran it at version 0. Exact same results from version 1.7.6 at version 180 and 1.7.13 at version 200. Rhino 1.7 release 5 PRERELEASE 2012 06 29
js> java.lang.Object.__javaObject__.getMethod('getClass').invoke('a' + new java.lang.String('b'))
class java.lang.String @victordiaz or @michbarsinai do you have any examples of where this is still broken for you? |
@tonygermano thanks for looking into this! Here's the problem: const e = [[some Java object whose .name property is the java.lang.String "ABC"]]
e.name=="ABC" // true
e.name==="ABC" // false
e.name=="A"+"BC" // true
e.name==="A"+"BC" // false Intuitively, I would expect all of the equality tests to be true (hope I'm correct here :-). I assume that has to do with ConsString, but I might be wrong. |
@michbarsinai Think this issue is about going from JavaScript > Java, whereas your example is from Java > JavaScript However, what you're pointing out does seem like a bug to me, created a separate case for this: #992 |
I stand corrected, with regards to the comment above: the behavior of isJavaPrimitiveWrap is the inversion of what I thought it would be: you need to set it to false to make sure Java primitives are exposed in JavaScript land as JavaScript primitives. So, there's no bug with regards to comment #247 (comment), it's just how you configure Rhino |
@michbarsinai Your current issue is mainly because when
Another issue with the same root cause for confusion is #891. You do have some options in javascript land: String(e.name) === "ABC" // explicit conversion to string primitive
e.name + '' === "ABC" // string concatenation always returns string primitive
e.name.equals("ABC") // embrace the java-ness Do not do I.e, in any javascript environment, not just Rhino, String("ABC") === "ABC" // true
new String("ABC") === "ABC" // false
new String("ABC").toString() === "ABC" // true
|
Setting the javaPrimitiveWrap property, which @p-bakker pointed out, to false is another option to have it automatically convert strings returned by java methods to primitives if this is desirable 100% of the time. The example below shows this behavior in the rhino shell, but you could also set it in java before starting to execute the javascript code. js> var o = new java.lang.Object()
js> o.toString()
java.lang.Object@326de728
js> typeof o.toString()
object
js> org.mozilla.javascript.Context.getCurrentContext().getWrapFactory().setJavaPrimitiveWrap(false)
js> o.toString()
java.lang.Object@326de728
js> typeof o.toString()
string
js> o.toString() === 'java.lang.Object@326de728'
true |
Thanks @p-bakker and @tonygermano for your inputs! I'll check this soon. |
As for the original reported issue: think the linked alfresco case contains the info to (try to) reproduce it |
This issue is still around on the latest master. Code to reproduce, based on the sample in the linked Alfresco case below. Albeit not exactly the same as the issue reported in the Alfresco case, where the NativeObject is passed as a param to a Java method, I think it demonstrates the issue package tmp;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.RhinoException;
import org.mozilla.javascript.ScriptableObject;
public class Test {
public static void main(String[] ar) {
final Context context = ContextFactory.getGlobal().enterContext();
context.setLanguageVersion(Context.VERSION_ES6);
ScriptableObject scope = context.initStandardObjects();
try {
test(context, scope);
} catch(RhinoException ex) {
System.out.println(ex.getScriptStackTrace());
} catch(Exception e) {
e.printStackTrace();
}finally {
Context.exit();
}
}
private static void test(Context cx, ScriptableObject scope) {
String evaluationScript = """
var df = new java.text.SimpleDateFormat('yyyy-MM-dd');
var theDate = df.format(new Date());
// FAILS
var stamptext = 'Example text ' + theDate.toString();
// WORKS:
//stamptext = ('Example text ' + theDate).toString();
var x = {
test: stamptext
};
x
""";
try {
NativeObject obj = (NativeObject) cx.evaluateString(scope, evaluationScript, "EvaluationScript", 1, null);
System.out.println(obj.get("test").getClass().getName()); // Map get
System.out.println(obj.get("test", obj).getClass().getName()); // Scriptable get
} catch (Exception e) {
e.printStackTrace();
}
}
} |
I don't think this is a bug. As reported, the problem was with sending a string from JS to a java method, which does appear to work correctly. The test in the previous comment is accessing properties of a NativeObject from java in the same way internal Rhino code would. Rhino would still need to perform a conversion step before passing these values back to Java, in which case it also knows which type a java method expects.
A javascript Likewise, you can't assume that a If you change the script in your test method like below, it will actually return a String evaluationScript = """
var x = {
test: 3.0
};
x
"""; So maybe the contract should just be that a javascript string primitive is a |
Have a look at the linked case from Alfresco: the issue there is sending a NativeObject into a Java method and then getting one of its properties in Java code, which is expected to be a String, but instead is a ConsString instance And prior to the implementation of Constring, it would of course return a String I'm not really sure that its expected behavior to have a Consstring instance leak out into Java code: ConsStrings are imho an internal thing to Rhino for optimizing string concatenation within scripting. Either as soon as they are stored somewhere (as key or value in a NativeObject/Map/Set/...) they should be stored as a regular String primitive. Or else, shouldn't at least the methods of the Map/List interface do the check and convert ConsString instances to Strings? |
I saw what was happening, but I don't think it's a bug. As you point out, it is sending a javascript object to a Java method, not a string. The javascript object is being sent as a NativeObject. A string (ConsString or not) would be sent as a String, especially if the java method signature was expecting a String, but I think also to minimize breaking changes for older implementations that accepted Objects and were trying to cast them directly to Strings. Once you start pulling data out of a NativeObject from Java, I don't think there is any contract that says everything that is returned will be automatically converted from javascript to java, because that is exactly how Rhino itself interacts with a NativeObject, and you wouldn't want all of those automatic conversions happening internally in the engine. The
I see this as similar to the getX methods on a JDBC ResultSet. The getObject method will return what it actually is, but getString will always return a string, even if the column is actually a non-string type. My suggestion (I think this can be accomplished with generics) is to allow: String strValue = Context.jsToJava(obj.get("test"), String.class);
Integer intValue = Context.jsToJava(obj.get("test"), Integer.class);
// instead of
String strValue = (String) Context.jsToJava(obj.get("test"), String.class);
Integer intValue = (Integer) Context.jsToJava(obj.get("test"), Integer.class); The get methods in NativeObject return Object. While a javascript number is usually a java.lang.Double, you might get something else which is still a Number but won't cast to a Double. In the same way, I think it would be fair to say while a javascript string is usually a java.lang.String, you might get something else which is still a CharSequence but won't cast to a String. I considered your last question about specifically addressing ConsString in the Map/List interfaces, which seems reasonable, but should numbers get the same treatment, then? Converting a ConsString to a String also changes the internal state of the ConsString. An odd thing I noticed is that the Map.get method is actually defined in ScriptableObject, even though only NativeObject actually implements Map. I don't see ScriptableObject.get called very much in the code base except in the NativeObject Map implementation itself, and a few other places that are likely calling the wrong method in the first place. var o = {value: ""};
while(next = getNext()) {
o.value += next;
} You'd have to be really careful that whatever changes were made didn't cause that loop to flatten the ConsString on every iteration. |
Well, agree with pretty much everything you say, except when it comes to ConsString :-) That JS numbers resolve to different Number implementations in Java land is just the nature of the game, but to me ConsString is really just an internal thing to Rhino and as soon as it gets stored as a key or value, I think it should be resolved to a String, so ConsString instances will never leak to Java code. Haven't tested it, just briefly looked at their code, but I think both Nashorn and GraalJS (both of which employ similar tactics to optimize String concatenation in JS I think) resolve their internal string representation to a String primitive when storing it as a key or value inside an Object/Map/Set/.... Curious what others think about this |
I think the fact that a javascript string is often compatible with a java String is also an implementation detail. The value I mentioned that is returned by If we do decide to make ConsStrings already get converted to Strings when being used as keys. The internal |
So, when a js object is sent to a java method from a js script or used as a return value from a script, what if instead of sending the actual internal object itself, it gets wrapped in a Scriptable that does perform conversion of values on its |
My 2¢ as a programmer that embeds Rhino in largerJava applications, and who also went a bit into Rhino code (not a very strong opinion but @p-bakker asked what others think): That being said, the point about number conversion is solid. I think there is a difference though, as Java does have a That being said, the above solution might fire back on equality tests and hashCodes for users, since conceptually people expect ConsString and String to be equal whenever they hold the same char sequence. So there are multiple aspects to be balanced here. I suppose the "principle of least surprise" is a good guideline in these cases. The JS side is more prone to surprises because of its dynamic type system, so that side might need more protection against surprises. |
I don't know if there is an API that dictates what should happen in this case. We are talking about a calling a Rhino method that is heavily used internally by the engine that gets a property by key and returns an Object representing a javascript (not Java) value. I'm wondering if a Wrapper (it doesn't actually have to be a Scriptable like I previously suggested) with a well-defined API to ease interacting with javascript objects from java should be the way to go, in the same way that we have a wrapper for interacting with java objects from javascript. A Context flag could optionally be used to automatically wrap js objects passed to java methods, but I don't see any reason why objects couldn't be manually wrapped (and unwrapped if needed) in java as well. The wrapper would also have to handle Callables among other things I'm probably not thinking of so they can be invoked from java. Javascript functions are not subclasses of NativeObject, and therefore do not implement Map, but they probably should since they are also js objects with enumerable properties. That could be solved by having the wrapper itself implement Map. Javascript arrays should implement Map in addition to List, as they are also objects that can have named as well as indexed keys. Pretty much anything that you can call Having a wrapper with a clean api would also ease the pain of making api breaking changes to the internal objects in the future as that would become the preferred way to interact with objects from java code.
|
Another surprise with calling any of the Considering a potential user API wrapper, I think the Map operations should continue to only deal with "own" properties, converting values as they are returned, but there should also be a different get method to allow walking the prototype chain. |
I've never used GraalVM, so I might be incorrect in the details. This says,
I can't find TruffleMap in the javadocs, though, so I think that might be dated information. I believe this is actually how javascript objects appear to java: https://www.graalvm.org/truffle/javadoc/org/graalvm/polyglot/Value.html It is a wrapper class that can contain many different types of values and creates a public API for working with them. The |
Sorry i was not able to follow the whole discussion but... Will try this in HtmlUnit and report my results. |
The gist of the discussion (imho) is whether ConsString is an internal thing of Rhino and this should never leak out into Java, or whether it's just another Type one can expect to get in Java from JavaScript. My position is the first, that is an internal thing, just to optimize string concatenation inside JavaScript and this should never leak to Java land. Not sure if removing ConsString is the solution: to my knowledge it provides significant performance improvements when concatenating strings in JavaScript. You're saying that you cannot see a way to prevent ConsString instances from leaking into Java, but aren't the only areas where that can still happen if ConsString instances are used as keys or values in JS Objects/Sets/Maps? If so, wouldn't that be relatively easy to fix? |
Ok, my guess about the performance impact was a bit too optimistic: have done some measurement with the HtmlUnit test suite running serveral test suites from real js frameworks shows a 25% downgrade of the performance. I think removing ConsString is not an options. @p-bakker regarding the conversion i like to have a look at the HtmlUnit code - there are at least some places where consstring's are handled from the java code. Maybe i can make a unit test to show the cases. And then we can discuss if the is correct/expected or not. |
Have checked the HtmlUnit (core-js) code and it looks like there is no special handling for ConsString - means ConsString seems not to leak into the java code for the cases we have tests for. |
My belated two cents on this -- I'm not surprised that ConsString has a
real performance impact, which is why I think the other JS engines do the
same thing. In a lot of cases, the existence of ConsString can be mitigated
in Java code by either making parameters take CharSequence instead of
String, or calling toString() on the result of a call to Rhino where a
String is required.
I think that part of this confusion is because the interface between
embedded Java code and Rhino itself is not always clear. I'm not sure what
we can do, but perhaps those of you who embed Java code in Rhino in
different ways may have some ideas. (The projects I have worked on that use
Rhino generally implement JavaScript objects in Java, usually using the old
"@JSFunction" and other annotations, so that they effectively give users a
JavaScript API that happens to be implemented in Java.)
…On Thu, Jul 22, 2021 at 10:02 AM RBRi ***@***.***> wrote:
Have checked the HtmlUnit (core-js) code and it looks like there is no
special handling for ConsString - means ConsString seems not to leak into
the java code for the cases we have tests for.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#247 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAD7I24RR4DRIWJPHF7YZP3TZBFK7ANCNFSM4BQ5K27Q>
.
|
A bit of topic: found two minor optimizations for our ConsString handling but have to do some testing first |
Have done a PR (#1000) containing the optimization and i found a place where we leak a ConsString into java. For me the case looks really like the one from this issue. More details in the PR. |
Haven't tested it, but just looking at the testcode in your PR, the PR doesn't seem to address the issue that initially triggered this case and is on display in #247 (comment) FWIW, the PR does seem to have its merits and looks good to me though |
I think one thing we can do is flatten ConsString instances when they are stored as keys or values in JavaScript Objects, Maps and Sets. Think Nashorn & GraalJS do that as well To other improvement could be making NativeString implement CharSequence, as suggested in one of the comments |
I pointed out already that keys are already flattened (at least for Objects; I did not look at Maps, but it would make sense to flatten them there if they currently are not,) and values should not be flattened or you can erase all of the performance benefits (I also provided an example where that would be the case.) From what I found of GraalVM, it defines an API specifically for passing cross-language values that lets you access them in a language independent way. There are methods to convert to native types, but they are not automatic. I don't know how Nashorn worked. PR (#1000) looked fine. The test cases involve a javascript string being returned from javascript to java and ensuring it has been converted to String if it was a ConsString. The case in question is when an Object is returned from javascript to java, and then java accesses properties of the javascript object, which may contain a ConsString, because the Object does not know it is being accessed from java. This is where GraalVM appears to wrap the Object in another API layer (an I lean towards documenting the behavior when the internal methods are used (which is also useful for Rhino developers) and providing a graalvm-like wrapper as an option for Scriptables that do not convert to "primitive" java types to do some of the auto-conversion for user-facing code. I think having a single java type with a well-defined API for handling dynamic js types which hides all of the implementation details is the right way to go for accessing values of non-primitive javascript types from java. The other detail of the graalvm polyglot value is that it is bound to a context, and if the context is closed, then all operations throw IllegalStateExceptions. I don't know if we would want to copy that behavior as well. |
As far as I understand, because of
LiveConnect
we should never haveorg.mozilla.javascript.ConsString
passed to the arguments of theJava host object
. And onlyjava.lang.String
should be passed instead.Currently, when in JS code I pass result of
'JavaScript string' + 'Java string'
as the arguments of the Java Host object's method, I haveorg.mozilla.javascript.ConsString
but notjava.lang.String
in my Java Code.Similar issues is described here:
https://issues.alfresco.com/jira/browse/ALF-20856
The text was updated successfully, but these errors were encountered: