2012-03-01 9 views
15

Esta pregunta es un corolario: Editing programs “while they are running”? Why?¿Edición de programas "mientras se están ejecutando"? ¿Cómo?

estoy sólo recientemente la exposición al mundo de Clojure y fascina afewexamples que he visto de "codificación en directo". La pregunta vinculada anteriormente discute el "por qué".

Mi pregunta es: ¿Cómo es esta técnica de codificación en vivo posible? ¿Es una característica del lenguaje de Clojure lo que lo hace posible? ¿O es solo un patrón que aplicaron y que podría aplicarse a cualquier idioma? Tengo experiencia en python y java. ¿Sería posible "codificar en vivo" en cualquiera de estos idiomas como es posible en Clojure?

+0

Relevante: https://en.wikipedia.org/wiki/Dynamic_software_updating – user

Respuesta

6

JRebel es una solución para Java. He aquí un breve pasaje de su FAQ:

JRebel se integra con la JVM y servidores de aplicaciones, principalmente en el nivel de cargador de clases. No crea ningún cargador de clases nuevo, sino que amplía los existentes con la capacidad de administrar clases recargadas.

3

Los conceptos se originaron en el mundo Lisp, pero cualquier idioma puede hacerlo (sin duda, si tiene una réplica, puede hacer este tipo de cosas). Es simplemente mejor conocido en el mundo de Lisp. Sé que hay paquetes slime-esque para Haskell y Ruby, y estaría muy sorprendido si tal cosa no existiera para Python también.

2

Es un patrón que se puede aplicar a cualquier idioma, siempre que el idioma se haya escrito con un entorno que permita reasignar nombres asociados con bloques de código.

En la computadora, el código y los datos existen en la memoria. En los lenguajes de programación, usamos nombres para referirnos a esos "trozos" de memoria.

"nombraría" cierta cantidad de bytes de memoria "a". También sería "ceder" que la memoria el valor del byte correspondiente a 0. Dependiendo del tipo de sistema,

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

sería "nombre" algún número de bytes de memoria "añadir". También "asignaría" esa memoria para contener las instrucciones de la máquina para buscar en la pila de llamadas dos números "int", agréguelos y coloque el resultado en el lugar apropiado de la pila de llamadas.

En un sistema de tipo que separa (y mantiene) nombres de bloques de código, el resultado final es que puede pasar fácilmente bloques de código por referencia, de la misma forma que puede tener memoria variable por referencia. La clave es asegurarse de que el sistema de tipos "coincida" solo con los tipos compatibles; de lo contrario, pasar alrededor de los bloques de código podría inducir a errores (como devolver un valor largo cuando se definió originalmente para devolver un int).

En Java, todos los tipos se resuelven en una "firma" que es una representación de cadena del nombre del método y "tipo". Mirando el ejemplo proporcionado complemento, la firma es

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

Si Java soportado (como se hace Clojure) Asignación de nombre de método, tendría que ampliarse en sus reglas de sistema tipo declarado, y permite asignar el nombre del método.Un ejemplo de falsa asignación método sería lógicamente parecerse

subtract = add; 

pero esto requeriría la necesidad de declarar restar, con una (para que coincida con Java) "tipo" inflexible.

public subtract(I,I)I; 

Y sin algún tipo de atención, tales declaraciones pueden recorrer fácilmente a las partes ya definidos de la lengua.

Pero para volver a su respuesta, en los idiomas que los admiten, los nombres son básicamente punteros a bloques de código, y pueden reasignarse siempre que no rompa las expectativas de los parámetros de entrada y retorno.

9

Algunas implementaciones de lenguaje tienen eso durante mucho tiempo, especialmente muchas variantes de Lisp y Smalltalk.

Lisp tiene identificadores como una estructura de datos, llamada símbolos. Estos símbolos se pueden reasignar y se buscan en el tiempo de ejecución. Este principio se llama último enlace. Símbolos de nombres de funciones y variables.

implementaciones Además Lisp, ya sea en tiempo de ejecución tienen un intérprete o incluso un compilador. La interfaz son las funciones EVAL y COMPILE. Además, hay una función LOAD, que permite cargar el código fuente y el código compilado.

Luego, un lenguaje como Common Lisp tiene un sistema de objetos que permite cambios en la jerarquía de clases, clases en sí mismas, puede agregar/actualizar/eliminar métodos y propaga estos cambios a objetos ya existentes. De modo que el software y el código orientados a objetos se pueden actualizar por sí mismos. Con el Meta-object Protocol, incluso se puede volver a programar el sistema de objetos en tiempo de ejecución.

También es importante que las implementaciones de Lisp puedan recolectar basura código eliminado. De esta forma, la ejecución de Lisp no crecerá en el tamaño de tiempo de ejecución solo porque se reemplaza el código.

Lisp a menudo también tiene un sistema de error que puede recuperarse de errores y permite reemplazar el código defectuoso dentro del depurador.

+0

Una buena descripción de lo que hace Lisp (con la terminología adecuada), pero un poco de luz en la parte de "cómo". –

+1

@Edwin Buck: para eso recomiendo leer cualquiera de los mejores libros que expliquen algún tipo de implementación de Lisp: Lisp en fragmentos pequeños, SICP, PAIP, ... –

2

Es posible en muchos idiomas, pero sólo si tiene las siguientes características:

  • Alguna forma de REPL o similares para que pueda interactuar con el entorno de ejecución
  • algún tipo de espacio de nombres que puede ser modificado en tiempo de ejecución
  • Enlace dinámico contra el espacio de nombres, por lo que si cambia los elementos en el espacio de nombres de código consultando a continuación, selecciona automáticamente el cambio

Lisp/Clojure tiene todos estos incorporados por defecto, que es una de las razones por las cuales es particularmente prominente en el mundo de Lisp.

ejemplo que demuestra estas características (todos en el 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 
4

Hay un montón de buenas respuestas aquí, y no estoy seguro de que puedo mejorar en cualquiera de ellos, pero yo quería añadir algunos comentarios sobre Clojure y Java.

En primer lugar, Clojure está escrito en Java, por lo que definitivamente puede crear un entorno de codificación en tiempo real en Java. Solo piense en Clojure como un sabor específico del entorno de codificación en vivo.

Básicamente, la codificación en tiempo real en Clojure funciona a través de la función de lectura en main.clj y la función eval en core.clj (src/clj/clojure/main.clj y src/clj/clojure/core.clj en el github repositorio). Usted lee en los formularios y los pasa a eval, que llama a clojure.lang.Compiler (src/jvm/clojure/lang/Compiler.java en el repositorio).

Compiler.java convierte formularios Clojure en bytecode JVM utilizando la biblioteca ASM (ASM website here, documentation here). No estoy seguro de qué versión de la biblioteca de ASM usa Clojure. Este bytecode (una matriz de bytes => byte [] bytecode es el miembro de la clase del compilador que finalmente tendrá los bytes generados por la clase clojure.asm.ClassWriter a través de ClassWriter # toByteArray) debe ser convertido a una clase y vinculado a el proceso de ejecución.

Una vez que tiene una representación de una clase como una matriz de bytes, se trata de obtener un java.lang.ClassLoader, llamar a defineClass para convertir esos bytes en una clase, y luego pasar la clase resultante a la resolución método del ClassLoader para vincularlo con el tiempo de ejecución de Java. Esto es básicamente lo que sucede cuando defines una nueva función, y puedes ver las partes internas del compilador en el compilador $ FnExpr, que es la clase interna que genera el bytecode para las expresiones de función.

Hay más cosas que eso con respecto a Clojure, como la forma en que maneja el espacio de nombres y el interinato de símbolos. No estoy completamente seguro de cómo se soluciona el hecho de que el ClassLoader estándar no reemplazará una clase vinculada con una nueva versión de esa clase, pero sospecho que tiene que ver con cómo se nombran las clases y cómo se internan los símbolos. Clojure también define su propio ClassLoader, un cierto clojure.lang.DynamicClassLoader, que hereda de java.net.URLClassLoader, por lo que podría tener algo que ver con él; No estoy seguro.

Al final, todas las piezas están ahí para hacer codificación en vivo en Java entre ClassLoaders y generadores de bytecode. Solo debe proporcionar una forma de ingresar formularios en una instancia en ejecución, evaluar los formularios y vincularlos.

Espero que arroje un poco más de luz sobre el tema.

2

Todo lo que se requiere es:

  • el lenguaje debe tener la capacidad de cargar nuevo código (eval)
  • una abstracción para redirigir las llamadas de función/método (VARs o mutables-espacios de nombres)
1

Sí, también es posible en otros idiomas. Lo hice en Python para un servidor en línea.

La característica clave necesaria es la capacidad de definir o redefinir nuevas funciones y métodos en tiempo de ejecución y esto es fácil con Python donde tiene "eval", "exec" y donde las clases y módulos son objetos de primera clase que pueden ser parcheado en tiempo de ejecución.

Lo implementé prácticamente al permitir una conexión de socket separada (por razones de seguridad solo desde la máquina local) aceptando cadenas y exec -ingún en el contexto del servidor en ejecución. Usando este enfoque, pude actualizar el servidor mientras se estaba ejecutando sin tener que desconectar a los usuarios conectados. El servidor estaba compuesto por dos procesos y era un campo de juego en línea con un cliente escrito en Haxe/Flash, que usaba una conexión de socket permanente para la interacción en tiempo real entre jugadores.

En mi caso usé esta posibilidad solo para algunas soluciones rápidas (la más grande era eliminar conexiones fantasmas que quedaban en caso de una desconexión de red en un estado de protocolo específico y también solucioné el error que permitió la creación de estas conexiones fantasmas).

También utilicé esta puerta trasera de administración para obtener información sobre el uso de recursos mientras el servidor se estaba ejecutando. Como nota curiosa, el primer error que arreglé en un servidor en ejecución fue un error en la maquinaria de puerta trasera (pero no estaba en línea con usuarios reales en ese caso, solo usuarios artificiales para pruebas de carga, por lo que era más como un cheque si pudiera hacerse más que un uso real ya que no habría habido ningún problema apagando el servidor para eso).

IMO la parte mala de hacer este tipo de pirateo en vivo es que una vez que arreglas la instancia en ejecución y puedes estar seguro de que la solución funciona, igual tienes que hacerlo en el código fuente normal y si la corrección no es Es trivial no puede estar 100% seguro de que la solución funcionará una vez que arranque una versión actualizada del servidor.

Incluso si su entorno le permite guardar la imagen parchada sin soltarla, aún así no puede estar seguro de que la imagen fija se iniciará o funcionará correctamente. La "corrección" en el programa en ejecución podría, por ejemplo, interrumpir el proceso de inicio, imposibilitando el correcto funcionamiento del programa.

Cuestiones relacionadas