Редактирование программ "пока они работают"? Как?

Этот вопрос является следствием: Редактирование программ "во время их работы" ? Почему?

Я только недавно столкнулся с миром Clojure и очарован a несколько примеры Я видел "живое кодирование". В приведенном выше вопросе обсуждается "почему".

Мой вопрос: как это возможно в режиме реального кодирования? Является ли это характеристикой языка Clojure, который позволяет это сделать? Или это просто шаблон, который они применили, который можно применить к любому языку? У меня есть фон в python и java. Было бы возможно "жить код" на любом из этих языков, как это возможно в clojure?

Ответы

Ответ 1

Некоторые языковые реализации имеют это долгое время, особенно много вариантов Lisp и Smalltalk.

Lisp имеет идентификаторы как структуру данных, называемые символами. Эти символы можно переназначить, и они будут проверены во время выполнения. Этот принцип называется поздним связыванием. Функции и переменные имен символов.

Кроме того, реализации Lisp либо имеют во время выполнения интерпретатор, либо даже компилятор. Интерфейсом являются функции EVAL и COMPILE. Плюс есть функция LOAD, которая позволяет загружать исходный код и скомпилированный код.

Далее язык, такой как Common Lisp, имеет объектную систему, которая позволяет изменять иерархию классов, сами классы, добавлять/обновлять/удалять методы и распространять эти изменения на уже существующие объекты. Таким образом, объектно-ориентированное программное обеспечение и код могут быть обновлены сами собой. С помощью протокола метаобъектов можно даже повторно запрограммировать объектную систему во время выполнения.

Также важно, чтобы реализации Lisp затем могли мусор собирать удаленный код. Таким образом, запуск Lisp не будет расти во время выполнения только потому, что код заменен.

Lisp часто также имеет систему ошибок, которая может восстанавливаться после ошибок и позволяет заменять дефектный код внутри отладчика.

Ответ 2

JRebel - одно из решений для Java. Вот краткий отрывок из FAQ:

JRebel интегрируется с серверами JVM и приложений главным образом на уровне загрузчика классов. Он не создает никаких новых загрузчиков классов, вместо этого он расширяет существующие с возможностью управления перезагруженными классами.

Ответ 3

Здесь есть много хороших ответов, и я не уверен, что могу улучшить их, но хотел бы добавить комментарии вокруг Clojure и Java.

Во-первых, Clojure написан на Java, поэтому вы можете определенно создать среду живого кодирования в Java. Просто подумайте о Clojure как о конкретном вкусе среды живого кодирования.

В основном, живое кодирование в Clojure работает через функцию read в main.clj и функцию eval в core.clj(src/clj/clojure/main.clj и src/clj/clojure/core.clj в репозитории github). Вы читаете в формах и передаете их в eval, который вызывает clojure.lang.Compiler(src/jvm/clojure/lang/Compiler.java в репо).

Compiler.java преобразует Clojure формы в байт-код JVM, используя библиотеку ASM (сайт ASM здесь, здесь). Я не уверен, какая версия библиотеки ASM используется clojure. Этот байт-код (массив байтов = > byte [] bytecode является членом класса Compiler, который в конечном итоге будет содержать байты, сгенерированные классом clojure.asm.ClassWriter через ClassWriter # toByteArray), затем должен быть преобразован в класс и связанный с текущим процессом.

Как только у вас есть представление класса в виде байтового массива, возникает вопрос о том, как получить java.lang.ClassLoader, вызывая defineClass, чтобы превратить эти байты в класс, а затем передать полученный класс методу разрешения ClassLoader, чтобы связать его со средой выполнения Java. В основном это происходит, когда вы определяете новую функцию, и вы можете увидеть внутренности компилятора в компиляторе $FnExpr, который является внутренним классом, который генерирует байт-код для выражений функций.

Здесь происходит больше, чем по отношению к Clojure, например, как он обрабатывает пространство имен и интернирование символов. Я не совсем уверен, как это связано с тем, что стандартный ClassLoader не заменит связанный класс новой версией этого класса, но я подозреваю, что он имеет отношение к тому, как названы классы и как интернационализируются символы. Clojure также определяет собственный ClassLoader, определенный clojure.lang.DynamicClassLoader, который наследует от java.net.URLClassLoader, так что это может иметь какое-то отношение к нему; Я не уверен.

В конце концов, все фрагменты должны делать живое кодирование в Java между ClassLoaders и генераторами байт-кода. Вам просто нужно предоставить способ ввода форм в исполняемый экземпляр, eval формы и связать их.

Надеюсь, что это проливает немного больше света на эту тему.

Ответ 4

Понятия возникли в мире Lisp, но практически любой язык может это сделать (конечно, если у вас есть repl, вы можете делать такие вещи). Он просто лучше известен в мире Lisp. Я знаю, что есть пакеты slime-esque для haskell и ruby, и я был бы очень удивлен, если бы такого не существовало и для Python.

Ответ 5

Это шаблон, который может быть применен к любому языку при условии, что язык был написан с помощью среды, которая позволяет переназначать имена, связанные с блоками кода.

В компьютере код и данные существуют в памяти. В языках программирования мы используем имена для обозначения этих "кусков" памяти.

int a = 0;

будет "называть" некоторое количество байтов памяти "a". Он также "назначил" этой памяти значение байта, соответствующее 0. В зависимости от системы типов

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

будет "называть" некоторое количество байтов памяти "добавить". Он также "назначил", чтобы память содержала машинные инструкции для поиска в стеке вызовов для двух чисел "int", добавляла их вместе и помещала результат в соответствующее место в стеке вызовов.

В системе типов, которая отделяет (и поддерживает) имена к блокам кода, конечный результат заключается в том, что вы можете легко передавать блоки кода вокруг ссылки гораздо таким же образом, как и переменная память вокруг ссылки. Ключ должен удостовериться, что система типов "соответствует" только совместимым типам, иначе прохождение вокруг блоков кода может вызвать ошибки (например, вернуть длинный, когда изначально было определено, чтобы вернуть int).

В Java все типы разрешают "подпись", которая представляет собой строковое представление имени метода и "type". Рассматривая представленный пример добавления, подпись

// 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;

И без некоторой осторожности такие объявления могут легко наступать на уже определенные части языка.

Но чтобы вернуться к вашему ответу, на языках, которые поддерживают такие, имена в основном являются указателями на блоки кода и могут быть переназначены, если вы не нарушаете ожиданий параметров ввода и возврата.

Ответ 6

Это возможно на многих языках, но только если у вас есть следующие функции:

  • Некоторая форма 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

Ответ 7

Все, что требуется:

  • язык должен иметь возможность загружать новый код (eval)
  • абстракция для перенаправления вызовов функции/метода (vars или пространства с изменяемыми именами)

Ответ 8

Да, это также возможно на других языках. Я сделал это в Python для онлайн-сервера.

Ключевой особенностью является способность определять или переопределять новые функции и методы во время выполнения, и это легко с Python, где у вас есть "eval", "exec" и где классы и модули являются первоклассными объектами, которые могут быть исправлены во время выполнения.

Я реализовал его практически, разрешив отдельное соединение сокетов (по соображениям безопасности только с локальной машины), принимающее строки, и exec -используя их в контексте работающего сервера. Используя этот подход, я смог обновить сервер во время его работы, не подключая подключенных пользователей к отключению. Сервер состоял из двух процессов и был онлайновым игровым полем с клиентом, написанным на Haxe/Flash, с использованием постоянного соединения сокета для взаимодействия в реальном времени между игроками.

В моем случае я использовал эту возможность только для некоторых быстрых исправлений (самый большой из них - удаление призрачных соединений, оставшихся в случае отключения сети в определенном состоянии протокола, и я также исправил ошибку, которая позволила создать этот призрак соединения).

Я также использовал этот бэкдор управления, чтобы получить информацию об использовании ресурсов во время работы сервера. Как забавная нота, самая первая ошибка, которую я исправил на работающем сервере, была ошибкой самого бэкдор-машинного оборудования (но в этом случае он не был онлайн с реальными пользователями, а просто искусственными пользователями для тестирования нагрузки, так что это больше похоже на проверку если бы это можно было сделать, чем реальное использование, так как для этого не было бы проблем вообще отключать сервер).

IMO, плохая часть такого рода взлома, заключается в том, что после исправления работающего экземпляра вы можете быть уверены, что исправление работает, вам все равно придется делать это в обычном исходном коде, и если исправление не является тривиальный, вы не можете быть на 100% уверены, что исправление будет работать после загрузки обновленной версии сервера.

Даже если ваша среда позволяет сохранить исправленное изображение, не сводя его вниз, вы все же не можете быть уверены, что фиксированное изображение запустится или будет работать правильно. "Исправить" в запущенной программе можно, например, разбить процесс запуска, что делает невозможным переход в правильное рабочее состояние.