这个问题是:Editing programs “while they are running”? Why?
的必然结果我最近才接触到Clojure的世界,并对a few examples我所看到的“现场编码”着迷。上面链接的问题讨论了“为什么。”
我的问题是: 这种实时编码技术可能如何?它是clojure语言的一个特征,使它成为可能吗?或者它只是一种他们应用的模式,可以应用于任何语言?我有python和java的背景知识。是否有可能使用这些语言中的“实时代码”,就像在clojure中一样?
答案 0 :(得分:9)
有些语言实现很长一段时间,特别是很多Lisp变种和Smalltalk。
Lisp将标识符作为数据结构,称为 symbols 。可以重新分配这些符号,并在运行时查找它们。这个原则叫做后期绑定。符号命名函数和变量。
此外,Lisp实现在运行时具有解释器或甚至编译器。界面是函数EVAL
和COMPILE
。另外还有一个函数LOAD
,它允许加载源代码和编译代码。
接下来,像Common Lisp这样的语言有一个对象系统,它允许更改类层次结构,类本身,可以添加/更新/删除方法并将这些更改传播到现有对象。因此,面向对象的软件和代码可以自行更新。使用元对象协议,甚至可以在运行时重新编程对象系统。
然后Lisp实现可以垃圾收集删除代码也很重要。这样,运行的Lisp不会因为代码被替换而在运行时大小上增长。
Lisp通常还有一个错误系统,可以从错误中恢复,并允许从调试器中替换有缺陷的代码。
答案 1 :(得分:6)
JRebel主要在类加载器级别与JVM和应用程序服务器集成。它不会创建任何新的类加载器,而是扩展现有的类加载器,并能够管理重新加载的类。
答案 2 :(得分:4)
这里有很多好的答案,我不确定我能否改进其中任何一个,但我想在Clojure和Java上添加一些评论。
首先,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 here,documentation here)将Clojure表单转换为JVM字节码。我不确定Clojure使用的是什么版本的ASM库。这个字节码(一个bytes => byte []字节码的数组是Compiler类的成员,它最终将通过ClassWriter#toByteArray保存clojure.asm.ClassWriter类生成的字节)然后必须转换为类并链接进入运行过程。
一旦将类表示为字节数组,就需要获取java.lang.ClassLoader,调用defineClass将这些字节转换为Class,然后将生成的Class传递给resolve方法ClassLoader将它链接到Java运行时。这基本上是在定义新函数时发生的情况,您可以在Compiler $ FnExpr中看到编译器的内部结构,它是生成函数表达式字节码的内部类。
与Clojure相比,还有更多的事情,例如它处理命名空间和符号实习的方式。我不能完全确定它是如何解决标准ClassLoader不会用该类的新版本替换链接类的事实,但我怀疑它与如何命名类以及如何实现符号有关。 Clojure还定义了自己的ClassLoader,一个clojure.lang.DynamicClassLoader,它继承自java.net.URLClassLoader,因此可能与它有关;我不确定。
最后,所有的部分都是在ClassLoader和字节码生成器之间用Java进行实时编码。您只需提供一种方法将表单输入到正在运行的实例中,评估表单并将它们链接起来。
希望这会对这个问题有所了解。
答案 3 :(得分:3)
这些概念起源于Lisp世界,但几乎任何语言都可以做到(当然,如果你有一个repl,你可以做这种事情)。它在Lisp世界中更为人所知。我知道有一些用于haskell和ruby的slime-esque包,如果Python中也不存在这样的东西,我会非常惊讶。
答案 4 :(得分:2)
这是一种可以应用于任何语言的模式,只要该语言是使用允许重新分配与代码块相关联的名称的环境编写的。
在计算机中,代码和数据存在于内存中。在编程语言中,我们使用名称来指代那些“内存块”。
int a = 0;
将“命名”一些字节的内存“a”。它还会“分配”该内存对应于0的字节值。取决于类型系统,
int add(int first, int second) {
return first + second;
}
将“命名”一些字节的内存“add”。它还会“分配”该内存以包含机器指令,以查看两个“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;
在没有一点小心的情况下,这样的声明可以很容易地依赖于已定义的语言部分。
但回到你的答案,在支持这种语言的语言中,名称基本上是指向代码块的指针,并且可以重新分配,只要你不打破输入和返回参数的期望。
答案 5 :(得分:2)
它可以在多种语言中使用,但前提是您具有以下功能:
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 :(得分:2)
所需要的只是:
答案 7 :(得分:1)
是的,它也可以用其他语言表达。我已经在Python中为在线服务器完成了它。
所需的关键功能是能够在运行时定义或重新定义新的函数和方法,这对于Python来说很容易,你有“eval”,“exec”,其中类和模块是可以修补的第一类对象在运行时。
我实际上通过允许单独的套接字连接(出于安全原因,仅来自本地机器)接受字符串并在运行的服务器的上下文中exec
来实现它。使用这种方法,我能够在服务器运行时更新服务器,而不会让连接的用户断开连接。该服务器由两个进程组成,是一个在线游戏场,其客户端使用Haxe / Flash编写,使用永久套接字连接实现玩家之间的实时交互。
在我的情况下,我使用这种可能性仅用于一些快速修复(最大的是删除在特定协议状态下网络断开连接时仍然存在的鬼连接,并且我还修复了允许创建这些鬼的错误连接)。
我还使用此管理后门在服务器运行时获取一些资源使用信息。作为一个有趣的说明,我在运行的服务器上修复的第一个错误是后门机器本身的一个错误(但在这种情况下它不是真正的用户在线,只是用于负载测试的人工用户,所以它更像是一个检查如果它可以完成而不是实际使用,因为根本没有问题关闭服务器。)
IMO执行此类实时黑客操作的一个不好的方面是,一旦您修复了正在运行的实例,并且您可以确定该修复程序有效,您仍然必须在常规源代码中执行此操作,如果修复程序不是在启动服务器的更新版本时,您无法100%确定修复程序是否有效。
即使您的环境允许保存已修补的图像而不将其删除,仍然无法确定修复的图像是否会启动或正常工作。正在运行的程序上的“修复”可能会破坏启动过程,从而无法达到正确的运行状态。