【问题标题】:GWT / JSNI - "DataCloneError - An object could not be cloned" - how do I debug?GWT / JSNI - “DataCloneError - 无法克隆对象” - 我该如何调试?
【发布时间】:2014-02-04 18:34:00
【问题描述】:

我正在尝试通过 JSNI 调用 parallels.js。 Parallels 为 web worker 提供了一个很好的 API,我编写了一些轻量级的包装代码,它为 GWT 的 worker 提供了比 Elemental 更方便的接口。但是我遇到了一个让我难过的错误:

com.google.gwt.core.client.JavaScriptException: (DataCloneError) @io.mywrapper.workers.Parallel::runParallel([Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;Lcom /google/gwt/core/client/JavaScriptObject;)([Java object: [Ljava.lang.String;@1922352522, JavaScript object(3006), JavaScript object(3008)]): 无法克隆对象。

这来自于托管模式:

在 com.google.gwt.dev.shell.ModuleSpaceOOPHM.doInvoke(ModuleSpaceOOPHM.java:136) 上的 com.google.gwt.dev.shell.BrowserChannelServer.invokeJavascript(BrowserChannelServer.java:249)。 gwt.dev.shell.ModuleSpace.invokeNative(ModuleSpace.java:571) 在 com.google.gwt.dev.shell.ModuleSpace.invokeNativeVoid(ModuleSpace.java:299) 在 com.google.gwt.dev.shell.JavaScriptHost。在 io.mywrapper.workers.Parallel.runParallel(Parallel.java) 处调用NativeVoid(JavaScriptHost.java:107)

这是我的代码:

创建工作者的示例客户端调用:

    Workers.spawnWorker(new String[]{"hello"}, new Worker() {
        @Override
        public String[] work(String[] data) {
            return data;
        }

        @Override
        public void done(String[] data) {
            int i = data.length;
        }
    });

提供通用接口的API:

public class Workers {

    public static void spawnWorker(String[] data, Worker worker) {
        Parallel.runParallel(data, workFunction(worker), callbackFunction(worker));
    }

    /**
     * Create a reference to the work function.
     */
    public static native JavaScriptObject workFunction(Worker worker) /*-{
        return worker == null ? null : $entry(function(x) {
            worker.@io.mywrapper.workers.Worker::work([Ljava/lang/String;)(x);
        });
    }-*/;

    /**
     * Create a reference to the done function.
     */
    public static native JavaScriptObject callbackFunction(Worker worker) /*-{
        return worker == null ? null : $entry(function(x) {
            worker.@io.mywrapper.workers.Worker::done([Ljava/lang/String;)(x);
        });
    }-*/;        
}

工人:

public interface Worker extends Serializable {

    /**
     * Called to perform the work.
     * @param data
     * @return
     */
    public String[] work(String[] data);

    /**
     * Called with the result of the work.
     * @param data
     */
    public void done(String[] data);

}

最后是 Parallels 包装器:

public class Parallel {

    /**
     * @param data Data to be passed to the function
     * @param work Function to perform the work, given the data
     * @param callback Function to be called with result
     * @return
     */
    public static native void runParallel(String[] data, JavaScriptObject work, JavaScriptObject callback) /*-{
        var p =  new $wnd.Parallel(data);
        p.spawn(work).then(callback);
    }-*/;
}

这是什么原因造成的?

JSNI 文档说,关于数组:

opaque value that can only be passed back into Java code

这很简洁,但最终我的数组会被传递回 Java 代码,所以我认为这些都可以。

编辑 - 好的,错误的假设。这些数组尽管表面上只是被传递回 Java 代码,但却导致了错误(这很奇怪,因为 DataCloneError 上几乎没有 googleability。)将它们更改为 String 是可行的;但是,String 不足以满足我的需求。看起来对象面临与数组相同的问题;我在另一个 StackOverflow 线程中看到了 Thomas 对 JSArrayUtils 的引用,但我不知道如何用字符串数组调用它(它需要一个 JavaScriptObjects 数组作为非原始类型的输入,这对我没有好处。)有什么巧妙的办法吗?

EDIT 2 - 在我使用 String[] 的任何地方都改为使用 JSArrayString。新问题;这次没有堆栈跟踪,但在控制台中我收到错误:未捕获的 ReferenceError:__gwt_makeJavaInvoke 未定义。当我在开发人员工具中单击生成脚本的 url 时,我得到了这个 sn-p:

self.onmessage = function(e) {self.postMessage((function (){
    try {
      return __gwt_makeJavaInvoke(3)(null, 65626, jsFunction, this, arguments);
    }
     catch (e) {
      throw e;
    }
  })(e.data))}

我看到 _gwt_makeJavaInvoke 是 JSNI 类的一部分;那为什么找不到呢?

【问题讨论】:

    标签: gwt jsni


    【解决方案1】:

    您可以在此处找到 GWT 和 WebWorkers 的工作示例:https://github.com/tomekziel/gwtwwlinker/

    这是一项初步工作,但使用此模式,我能够使用 AutoBeanFactory 提供的序列化将 GWT 对象传入和传出 webworker。

    【讨论】:

      【解决方案2】:

      如果您从不使用开发模式,则当前可以安全地假装 Java String[] 是一个包含字符串的 JS 数组。这将在开发模式下中断,因为数组必须在 Java 中可用并且字符串被特殊处理,并且如果编译器以不同方式优化数组,将来可能会中断。

      将来可能出错的情况:

      Java 数组和 JavaScript 数组的语义不同 - Java 数组无法调整大小,并且根据组件类型(数组中的数据)使用特定值进行初始化。由于您正在编写 Java 代码,因此编译器可能会根据有关您如何创建和使用该数组的详细信息做出假设,而该数组可能会被不知道永远不会修改数组的 JS 代码破坏。

      一些原始类型的数组可以在 JavaScript 中优化为 TypedArrays,在调整大小方面更接近 Java 语义,在分配方面更接近 Java 行为。这也会提高性能,但可能会破坏 int[]double[] 等的任何使用。


      相反,您应该将数据复制到JsArrayString 中,或者只使用 js 数组来保存数据而不是来回移动,具体取决于您的用例。各种 JsArray 类型可以调整大小,并且已经作为 JS 外部可以理解和使用的 JavaScript 对象存在。


      回复编辑 2:

      据推测,parallel.js 脚本正在尝试从另一个范围运行您的代码,例如在您的 GWT 代码不存在的 webworker(这就是代码的重点,对)中。因此,它不能调用作为返回开发模式的桥梁的 makeJavaInvoke(对于已编译的 JS 将是不同的失败)。根据http://adambom.github.io/parallel.js/,传递的回调必须满足特定要求才能传递给spawn,也许then - 您的匿名函数肯定不满足它们,并且可能无法维护java语义。

      在深入了解之前,请查看我不久前针对 webworkers 和 gwt/java 的基本问题所做的回答:https://stackoverflow.com/a/11376059/860630

      如上所述,WebWorkers 实际上是新进程,与原始进程没有共享代码或共享状态。 Parallel.js 代码试图用一些技巧来掩盖这一点 - 共享状态以传递给原始 Parallel 构造函数的内容的形式提供,但您正在尝试 传入“java”对象的实例并调用它们的方法。这些 Java 实例带有自己的状态,并且可能通过 Worker 实例中的字段链接回 Java 应用程序的其余部分。如果我正在实现 Worker 并做一些引用其他数据而不是传入的数据的事情,那么我会看到更多奇怪的失败。

      所以你传入的函数必须是完全独立的——它们不能以任何方式引用外部代码,因为这样就不能将函数传递给 webworker 或 几个 webworkers ,彼此都不知道对方的存在。参见https://github.com/adambom/parallel.js/issues/32 例如:

      这是不可能的,因为它会

      • 要求工作人员之间共享状态
      • 要求我们传输所有范围变量(我认为甚至不可能读取可用范围)

      唯一可能的可能是缓存变量,但这些变量已经可以在函数本身中使用 spawn() 定义,并且在 map 中没有任何意义(因为没有共享状态)。

      实际上并不熟悉parallel.js 是如何实现的(到目前为止,所有这些答案都是阅读文档并在谷歌上快速搜索“parallel.js 共享状态”,并且已经使用WebWorkers 进行了一天左右的实验,并且决定我现在的问题还不值得费心),我猜then 是不受限制的,你可以随意传递它,但是spawnmapreduce 必须写这样他们的 JS 就可以传递给新的 JS 进程并完全独立在那里。

      可能在编译时可以从您的普通 Java 代码中实现,前提是您只有一个 Worker 实现并且该 impl 从不使用除了直接传入的状态之外的状态。在这种情况下,编译器应该将您的方法重写为静态的,以便它们在这种情况下可以安全使用。但是,这并不是一个非常有用的库,因为您似乎正在努力实现这一目标。考虑到这一点,您可以将工作代码保留在 JSNI 中,以确保您遵循 parallel.js 规则。

      最后,根据正常的 GWT 规则,避免 $entry 调用您希望在其他上下文中发生的调用,因为这些工作人员无法访问 $entry 启用的正常异常处理和调度。

      (最后,如果您在编写 Worker 实现时非常小心,并且编写了一个以非常具体的方式调用每个 Worker 实现的生成器,以确保 com.google.gwt.dev.jjs.impl.MakeCallsStaticcom.google.gwt.dev.jjs.impl.Pruner 可以正确地执行将这些实例方法中的this 删除,一旦它们被重写为 JS 函数。我认为最干净的方法是在生成器本身中发出 JSNI,调用编写的静态方法在真正的 Java 中,并从该静态方法调用特定的实例方法,该方法为 spawn 等做繁重的工作。)

      【讨论】:

      • 谢谢。那么在 Java 代码中创建和填充 JsArrayString 是合法的吗?我可以将它从 Java 传递到 Javascript,然后返回?
      • 是的,没错——任何 JSO(JavaScriptObject 子类)都可以在 Java/JSNI/JavaScript 之间传递,并像真正属于那里一样使用。唯一的限制是在 JSO 的声明(和实例化)中,它们必须有一个受保护的 ctor 并且只能在 JS/JSNI 中创建,所有方法都必须是最终的。哦,你不能从 JSNI 内部调用 JSO 方法,尽管这很少是一个问题,因为你可以调用 JS 版本。使用 JsArrayString array = JavaScriptObject.createArray().cast() 创建它,并使用类似 java 的方法进行填充。
      • 谢谢,我越来越近了。见我上面的“编辑2”;我已将我的代码更改为使用 JSArrayString,而我之前使用的是 String[],现在我看到了同样无法通过 Google 搜索的“未捕获的 ReferenceError:__gwt_makeJavaInvoke 未定义”。 (我是不是特别不走运,还是没有很多人做这件事?)关于导致这种情况的任何想法?
      • 我会尝试在原始答案中作为一个新部分来回答,因为它太长了,不适合这里。
      • 很棒的答案,谢谢。我真的有两个问题,我应该只专注于第一个:a)我需要在客户端生成大素数; b) 我对 GWT 中缺少通用选项感到沮丧(gwt-ns 不再维护;gwtwwlinker 是一个很好的起点,但不是通用的)。我完全忽略了 GWT 不会出现在工作“JVM”中的事实,除非我当然将它指向第二个模块。所以,我认为结果是:首先通过工人的素数,然后我会回过头来概括。再次感谢,你很有帮助。
      猜你喜欢
      • 2017-11-07
      • 2020-10-22
      • 2015-02-17
      • 1970-01-01
      • 2019-04-30
      • 2021-03-28
      • 2013-03-09
      • 2015-02-19
      相关资源
      最近更新 更多