Редактировать программы «пока они работают»?Как? - PullRequest
15 голосов
/ 01 марта 2012

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

Я только недавно столкнулся с миром Clojure и очарован a несколькими примерами I 'видел "живое кодирование".Приведенный выше вопрос обсуждает вопрос «почему».

Мой вопрос: Как возможен этот метод живого кодирования?Это характерная черта языка clojure, которая делает это возможным?Или это просто шаблон, который они применили, который может быть применен к любому языку?У меня есть опыт работы с Python и Java.Будет ли возможно «живой код» на любом из этих языков, как это возможно в clojure?

Ответы [ 8 ]

9 голосов
/ 02 марта 2012

У некоторых языковых реализаций это уже давно, особенно у многих вариантов Lisp и Smalltalk.

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

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

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

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

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

6 голосов
/ 01 марта 2012

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

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

4 голосов
/ 03 марта 2012

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

Прежде всего, Clojure написан на Java, так что вы точно можете создать среду живого программирования на Java. Просто подумайте о Clojure как об особой разновидности среды живого программирования.

По сути, живое кодирование в Clojure работает через функцию чтения в 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. Этот байт-код (массив bytes => byte [] bytecode является членом класса Compiler, который в конечном итоге будет содержать байты, сгенерированные классом clojure.asm.ClassWriter через ClassWriter # toByteArray), затем должен быть преобразован в класс и связан с запущенный процесс.

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

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

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

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

3 голосов
/ 01 марта 2012

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

2 голосов
/ 03 марта 2012

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

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

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

  • Некоторая форма 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
2 голосов
/ 01 марта 2012

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

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

int a = 0;

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

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

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

В системе типов, которая разделяет (и поддерживает) имена на блоки кода, конечным результатом является то, что вы можете легко передавать блоки кода по ссылке во многом так же, как вы можете изменять память по ссылке. Главное - убедиться, что система типов «соответствует» только совместимым типам, в противном случае передача вокруг блоков кода может вызвать ошибки (например, возвращение long, если изначально определено, чтобы возвращать int).

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

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

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

1 голос
/ 05 марта 2012

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

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

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

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

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

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

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

...