【问题标题】:Editing programs “while they are running”? How?“在程序运行时”编辑程序?如何?
【发布时间】:2012-03-20 03:14:26
【问题描述】:

这个问题是对Editing programs “while they are running”? Why?的推论

我最近才接触到 Clojure 的世界,并且对 a few examples 着迷,我看到了“实时编码”。上面链接的问题讨论了“为什么”。

我的问题是:如何这种实时编码技术是可能的?是 clojure 语言的一个特性使它成为可能吗?或者它只是他们应用的一种模式,可以应用于任何语言?我有python和java的背景。是否可以像在 clojure 中一样使用这两种语言中的任何一种“实时代码”?

【问题讨论】:

标签: java python clojure lisp scheme


【解决方案1】:

JRebel 是 Java 的一种解决方案。这是他们FAQ的简短段落:

JRebel 主要在类加载器级别与 JVM 和应用程序服务器集成。它不会创建任何新的类加载器,而是通过管理重新加载的类的能力扩展现有的类加载器。

【讨论】:

    【解决方案2】:

    这些概念起源于 Lisp 世界,但几乎任何语言都可以做到(当然,如果你有一个 repl,你就可以做到这一点)。它在 Lisp 世界中更为人所知。我知道有用于 haskell 和 ruby​​ 的 slime-esque 包,如果 Python 也不存在这样的东西,我会感到非常惊讶。

    【讨论】:

      【解决方案3】:

      这是一种可以应用于任何语言的模式,前提是该语言是在允许重新分配与代码块关联的名称的环境中编写的。

      在计算机中,代码和数据存在于内存中。在编程语言中,我们使用名称来指代那些内存“块”。

      int a = 0;
      

      将“命名”一些内存字节数“a”。它还将“分配”该内存对应于 0 的字节值。根据类型系统,

      int add(int first, int second) {
        return first + second;
      }
      

      将“命名”一些内存字节数“添加”。它还将“分配”该内存以包含机器指令,以在调用堆栈中查找两个“int”数字,将它们相加,并将结果放在调用堆栈上的适当位置。

      在将名称分隔(并维护)代码块的类型系统中,最终结果是您可以轻松地通过引用传递代码块,就像您可以通过引用改变内存一样。关键是要确保类型系统只“匹配”兼容的类型,否则传递代码块可能会导致错误(比如在最初定义为返回 int 时返回 long)。

      在 Java 中,所有类型都解析为“签名”,它是方法名称和“类型”的字符串表示形式。查看提供的添加示例,签名是

      // This has a signature of "add(I,I)I"
      int add(int first, int second) {
        return first + second;
      }
      

      如果 Java 支持(如 Clojure 那样)方法名分配,它必须扩展其声明的类型系统规则,并允许方法名分配。一个假的方法分配示例在逻辑上看起来像

      subtract = add;
      

      但这需要使用强类型(以匹配 Java)“类型”声明减法。

      public subtract(I,I)I;
      

      如果不小心,这样的声明很容易踩到语言中已经定义的部分。

      但要回到您的答案,在支持此类的语言中,名称基本上是指向代码块的指针,并且可以重新分配,前提是您不破坏输入和返回参数的期望。

      【讨论】:

        【解决方案4】:

        一些语言实现已经有很长时间了,尤其是许多 Lisp 变体和 Smalltalk。

        Lisp 将标识符作为一种数据结构,称为符号。可以重新分配这些符号,并在运行时查找它们。这个原则被称为后期绑定。符号命名函数和变量。

        另外,Lisp 实现在运行时有一个解释器,甚至有一个编译器。接口是函数EVALCOMPILE。另外还有一个函数LOAD,可以加载源代码和编译代码。

        接下来,像 Common Lisp 这样的语言有一个对象系统,它允许更改类层次结构、类本身,可以添加/更新/删除方法并将这些更改传播到现有对象。所以面向对象的软件和代码可以自己更新。使用元对象协议,我们甚至可以在运行时重新编程对象系统。

        同样重要的是 Lisp 实现可以垃圾收集删除代码。这样,运行中的 Lisp 就不会因为代码被替换而增加运行时大小。

        Lisp 通常还有一个错误系统,它可以从错误中恢复并允许从调试器中替换有缺陷的代码。

        【讨论】:

        • 很好地描述了 Lisp 所做的事情(使用适当的术语),但对“如何”部分稍加说明。
        • @Edwin Buck:为此,我建议阅读任何解释某种 Lisp 实现的更好的书籍:Lisp in small pieces, SICP, PAIP, ...
        【解决方案5】:

        在许多语言中都可以使用,但前提是您具有以下功能:

        • 某种形式的 REPL 或类似形式,以便您可以与运行环境进行交互
        • 可以在运行时修改的某种形式的命名空间
        • 针对命名空间的动态绑定,因此如果您更改命名空间中的项目,那么引用代码会自动获取更改

        Lisp/Clojure 默认内置了所有这些,这也是它在 Lisp 世界中特别突出的原因之一。

        演示这些功能的示例(全部在 Clojure REPL 中):

        ; define something in the current namespace
        (def y 1)
        
        ; define a function which refers to y in the current namespace
        (def foo [x] (+ x y))
        
        (foo 10)
        => 11
        
        ; redefine y
        (def y 5)
        
        ; prove that the change was picked up dynamically
        (foo 10)
        => 15
        

        【讨论】:

          【解决方案6】:

          这里有很多好的答案,我不确定我是否可以改进其中任何一个,但我想围绕 Clojure 和 Java 添加一些 cmets。

          首先,Clojure 是用 Java 编写的,因此您绝对可以用 Java 构建实时编码环境。只需将 Clojure 视为一种特殊的实时编码环境。

          基本上,Clojure 中的实时编码通过 main.clj 中的 read 函数和 core.clj 中的 eval 函数(github 存储库中的 src/clj/clojure/main.clj 和 src/clj/clojure/core.clj )。您阅读表单并将它们传递给 eval,后者调用 clojure.lang.Compiler(repo 中的 src/jvm/clojure/lang/Compiler.java)。

          Compiler.java 使用 ASM 库(ASM website heredocumentation here)将 Clojure 表单转换为 JVM 字节码。我不确定 Clojure 使用的是哪个版本的 ASM 库。这个字节码(一个字节数组 => byte[] 字节码是编译器类的成员,它最终将保存由 clojure.asm.ClassWriter 类通过 ClassWriter#toByteArray 生成的字节)然后必须转换为一个类并链接到正在运行的进程。

          一旦你将一个类表示为一个字节数组,就需要获取一个 java.lang.ClassLoader,调用 defineClass 将这些字节转换为一个 Class,然后将生成的 Class 传递给 resolve 方法ClassLoader 将其链接到 Java 运行时。这基本上是当你定义一个新函数时发生的事情,你可以在 Compiler$FnExpr 中看到编译器的内部结构,它是为函数表达式生成字节码的内部类。

          除了 Clojure 之外,还有更多事情要做,例如它处理命名空间和符号实习的方式。我不完全确定它是如何解决标准 ClassLoader 不会用该类的新版本替换链接类的事实,但我怀疑它与类的命名方式和符号的实习方式有关。 Clojure 还定义了自己的 ClassLoader,某个 clojure.lang.DynamicClassLoader,它继承自 java.net.URLClassLoader,所以这可能与它有关;我不确定。

          最后,所有部分都可以在 Java 中在 ClassLoader 和字节码生成器之间进行实时编码。您只需提供一种将表单输入到正在运行的实例中、评估表单并将它们链接起来的方法。

          希望这能对这个主题有更多的了解。

          【讨论】:

            【解决方案7】:

            只需要:

            • 该语言必须能够加载新代码 (eval)
            • 重定向函数/方法调用(变量或可变命名空间)的抽象

            【讨论】:

              【解决方案8】:

              是的,其他语言也可以。我已经在 Python 中为在线服务器完成了它。

              所需的关键特性是在运行时定义或重新定义新函数和方法的能力,这对于 Python 来说很容易,因为你有“eval”、“exec”,并且类和模块是可以修补的一流对象在运行时。

              我实际上是通过允许单独的套接字连接(出于安全原因仅从本地计算机)接受字符串并在运行服务器的上下文中exec-ing 来实现它。使用这种方法,我能够在服务器运行时更​​新服务器,而不会让连接的用户断开连接。服务器由两个进程组成,是一个在线游戏场,客户端使用 Haxe/Flash 编写,使用永久套接字连接进行玩家之间的实时交互。

              在我的情况下,我仅将这种可能性用于一些快速修复(最大的是删除在特定协议状态下网络断开连接的情况下剩余的幽灵连接,我还修复了允许创建这些幽灵的错误连接)。

              我还在服务器运行时使用了这个管理后门来获取一些资源使用信息。有趣的是,我在运行的服务器上修复的第一个错误是后门机器本身的错误(但在这种情况下,它并没有与真实用户在线,只是用于负载测试的人工用户,所以它更像是一个检查如果它可以完成而不是真正的用途,那么关闭服务器就没有问题了)。

              IMO 进行这种实时黑客攻击的不利之处在于,一旦您修复了正在运行的实例并且您可以确定修复工作,您仍然必须在常规源代码中进行,如果修复不微不足道,您无法 100% 确定在您启动更新版本的服务器后修复会起作用。

              即使您的环境允许保存修补后的图像而不将其关闭,您仍然无法确定修复后的图像会启动或正常工作。例如,正在运行的程序上的“修复”可能会破坏启动过程,使其无法进入正确的运行状态。

              【讨论】:

                猜你喜欢
                • 2011-07-01
                • 2012-08-15
                • 1970-01-01
                • 1970-01-01
                • 2014-08-31
                • 2018-12-31
                • 2013-06-24
                • 2022-01-12
                • 1970-01-01
                相关资源
                最近更新 更多