Это первая статья из цикла “Разра­ба­тываем свой язык програм­ми­ро­вания на Java”, который на примере разра­ботки простого языка, покажет полный путь создания языка, а также написания и поддержки инстру­ментов для него. К концу данной статьи реализуем интер­пре­татор, с помощью которого можно будет выполнять программы на нашем языке.

Любой язык програм­ми­ро­вания имеет синтаксис, который необходимо преоб­ра­зовать в удобную для валидации, преоб­ра­зо­вания и испол­нения структуру данных. Как правило, такой струк­турой данных выступает абстрактное синтак­си­ческое дерево (abstract syntax tree, AST). Каждый узел дерева обозначает конструкцию, встре­ча­ю­щуюся в исходном коде. Исходный код разби­рается парсером и на выходе получается AST.

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

В данной статье рассмотрим инструмент ANTLR. ANTLR – утилита, на вход которой подается грамматика в виде РБНФ, а на выходе получаем интерфейсы/классы (в нашем случае это java-код) для разбора программ. Список языков, на которых генери­руются парсеры, можно найти здесь.

Пример грамматики

Перед тем как перейти к реальной грамматике, попробуем на словах описать некоторые правила типичного языка программирования:

  • ПЕРЕМЕННАЯ – это ИДЕНТИФИКАТОР
  • ЦИФРА – это один из символов 0 1 2 3 4 5 6 7 8 9
  • ЧИСЛО – это один или более элементов типа ЦИФРА
  • ВЫРАЖЕНИЕ – это ЧИСЛО
  • ВЫРАЖЕНИЕ – это ПЕРЕМЕННАЯ
  • ВЫРАЖЕНИЕ – это ВЫРАЖЕНИЕ ‘+’ ВЫРАЖЕНИЕ
  • ВЫРАЖЕНИЕ – это ‘(‘ ВЫРАЖЕНИЕ ’)’

Как видно из данного списка, языковая грамматика – это набор правил, которые могут иметь рекур­сивные связи. Каждое правило может ссылаться на себя или на любое другое правило. ANTLR в своем арсенале имеет множество опера­торов для описания таких правил.

: метка начала правила
; метка конца правила
| оператор альтернативы
.. оператор диапазона
~ отрицание
. любой символ
= присваивание
(...) подправило
(...)* повторение подправила 0 или более раз
(...)+ Повторение подправила 1 или более раз
(...)? подправило, может отсутствовать
{...} семантические действия (на языке, использующемся в качестве выходного – например, Java)
[...] параметры правила

Примеры правил на ANTLR

Следующий пример описывает правила для целых чисел и чисел с плавающей точкой:

NUMBER : [0-9]+ ;
FLOAT  : NUMBER '.' NUMBER ;

Очень важно понимать, что грамматика описывает только синтаксис языка, на основе которого будет генери­ро­ваться парсер. Парсер будет генери­ровать AST, используя который, можно будет реали­зовать семантику языка. В преды­дущем примере, мы задали правило для разбора целого числа, но не описали какой объем памяти занимает число (8 бит, 16, …), является ли число со знаком или без. Например, в некоторых языках програм­ми­ро­вания переменную можно начать исполь­зовать, не объявив ее. Также можно не объявлять тип переменной, в этом случае тип будет определен автома­ти­чески в рантайме. Все эти правила семантики языка не описы­ваются в грамматике, а реали­зуются в другой части языка.

Лексемы и выражения ANTLR

Грамматика ANTLR состоит из двух типов правил: лексем и выражений, которые исполь­зуются для опреде­ления структуры грамматики и разбора входных данных.

Лексемы (или токены) – это правила, которые определяют отдельные лекси­ческие элементы входного языка, такие как числа, иденти­фи­каторы, знаки операций и т.д. Каждой лексеме соответ­ствует опреде­ленный тип токена, который исполь­зуется для дальнейшей обработки синтак­си­ческим анали­за­тором. Лекси­ческий анали­затор сканирует входной текст, разбивает его на лексемы и создает после­до­ва­тель­ность токенов, которые затем передаются синтак­си­че­скому анали­затору. Записы­ваются в верхнем регистре (Например: NUMBERIDENTIFIER).

Выражения – это правила, которые определяют структуру грамматики входного языка. Они описывают, каким образом лексемы связаны между собой и как они могут быть объединены в более сложные конструкции. Выражения могут содержать ссылки на лексемы, а также на другие выражения. Записы­ваются в нотации сamelCase (Например: expressionfunctionDefinition).

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

Требо­вания к языку

Перед тем как начать реали­зо­вывать язык необходимо опреде­литься с набором возмож­ностей, которые он должен поддер­живать. Для нашей задачи, в образо­ва­тельных целях, мы будем исполь­зовать простую грамматику. Язык будет поддер­живать следующие конструкции:

  • Переменные (типов StringLongDouble);
  • Оператор присва­и­вания (=);
  • Арифме­ти­ческие операции (+, -, *, /);
  • Операторы сравнения (>, <, >=, <=, ==, !=);
  • Условные операторы (if, else);
  • Функции;
  • Печать в консоль (встро­енный оператор println).

Грамматика

Ну и наконец, полное описание грамматики для языка:

grammar Jimple;

// корневое правило грамматики
program: (statement)* EOF;

// список возможных утверждений
statement: variableDeclaration
| assignment
| functionDefinition
| functionCall
| println
| return
| ifStatement
| blockStatement
;

// список возможных выражений
expression: '(' expression ')' #parenthesisExpr
| left=expression op=(ASTERISK | SLASH) right=expression #mulDivExpr
| left=expression op=(PLUS | MINUS) right=expression #plusMinusExpr
| left=expression compOperator right=expression #compExpr
| IDENTIFIER #idExp
| NUMBER #numExpr
| DOUBLE_NUMBER #doubleExpr
| STRING_LITERAL #stringExpr
| functionCall #funcCallExpr
;

// описания отдельных выражений и утверждений
variableDeclaration: 'var' IDENTIFIER '=' expression ;
assignment: IDENTIFIER '=' expression ;
compOperator: op=(LESS | LESS_OR_EQUAL | EQUAL | NOT_EQUAL | GREATER | GREATER_OR_EQUAL) ;
println: 'println' expression ;
return: 'return' expression ;
blockStatement: '{' (statement)* '}' ;
functionCall: IDENTIFIER '(' (expression (',' expression)*)? ')' ;
functionDefinition: 'fun' name=IDENTIFIER '(' (IDENTIFIER (',' IDENTIFIER)*)? ')' '{' (statement)* '}' ;
ifStatement: 'if' '(' expression ')' statement elseStatement? ;
elseStatement: 'else' statement ;

// список токенов
IDENTIFIER : [a-zA-Z_] [a-zA-Z_0-9]* ;
NUMBER : [0-9]+ ;
DOUBLE_NUMBER : NUMBER '.' NUMBER ;
STRING_LITERAL : '"' (~["])* '"' ;
ASTERISK : '*' ;
SLASH : '/' ;
PLUS : '+' ;
MINUS : '-' ;
ASSIGN : '=' ;
EQUAL : '==' ;
NOT_EQUAL : '!=' ;
LESS : '<' ;
LESS_OR_EQUAL : '<=' ;
GREATER : '>' ;
GREATER_OR_EQUAL : '>=' ;
SPACE : [ \r\n\t]+ -> skip;
LINE_COMMENT : '//' ~[\n\r]* -> skip;

Как вы уже, наверное, догадались, наш язык называется Jimple (проис­ходит от Jvm Simple). Пожалуй, стоит объяснить некоторые моменты, которые могут быть неоче­видными при первом знакомстве с ANTLR.

Метки

При описании правил некоторых операций была исполь­зована метка op. Это позволяет нам в дальнейшем исполь­зовать эту метку в качестве имени переменной, которая будет содержать значение оператора. В принципе, можно было бы обойтись без указания меток, но в таком случае придется писать допол­ни­тельный код, чтобы достать значение оператора из дерева разбора.

compOperator: op=(LESS | LESS_OR_EQUAL | EQUAL | NOT_EQUAL | GREATER | GREATER_OR_EQUAL) ;

Имено­ванные альтер­нативы правил

В ANTLR при опреде­лении правила с несколькими альтер­на­тивами каждому из них можно дать имя, и тогда в дереве это будет отдельный узел обработки. Что очень удобно когда нужно вынести обработку каждого варианта правила в отдельный метод. Важно, что наиме­но­вания нужно задать либо всем альтер­на­тивам, либо ни одной из них. Следующий пример демон­стрирует, как это выглядит:

expression: '(' expression ')' #parenthesisExpr
 | IDENTIFIER #idExp
 | NUMBER #numExpr

ANTLR сгене­рирует следующий код:

public interface JimpleVisitor<T> {
    T visitParenthesisExpr(ParenthesisExprContext ctx);
    T visitIdExp(IdExpContext ctx);
    T visitNumExpr(NumExprContext ctx);
}

Каналы

В ANTLR существует такая конструкция как канал (channel). Обычно каналы исполь­зуются для работы с коммен­та­риями, но так как в большинстве случаев нам не нужно проверять наличие коммен­тариев их необходимо отбросить с помощью -> skip, чем мы и восполь­зо­вались. Однако, бывают случаи, когда нам нужно интер­пре­ти­ровать значение коммен­тариев или других конструкций, тогда вы исполь­зуете каналы. В ANTLR уже есть встро­енный канал под названием HIDDEN, который вы можете исполь­зовать, или объявить свои каналы для опреде­ленных целей. Далее при разборе кода можно будет получить доступ к этим каналам.

Пример объяв­ления и исполь­зо­вания канала

channels { MYLINECOMMENT }

LINE_COMMENT : '//' ~[rn]+ -> channel(MYLINECOMMENT) ;

Фрагменты

На ряду с токенами (лексемами) в ANTLR присут­ствует такое понятие как фрагменты (fragment). Правила с префиксом фрагмента могут быть вызваны только из других правил лексера. Они не являются токенами сами по себе. В следующем примере мы вынесли во фрагменты опреде­ления чисел для разных систем счисления.

NUMBER: DIGITS | OCTAL_DIGITS | HEX_DIGITS;
fragment DIGITS: '1'..'9' '0'..'9'*;
fragment OCTAL_DIGITS: '0' '0'..'7'+;
fragment HEX_DIGITS: '0x' ('0'..'9' | 'a'..'f' | 'A'..'F')+;

Таким образом, число в любой системе счисления (например: «123», «0762» или «0xac1») будет рассмат­ри­ваться как токен NUMBER, а не DIGITS, OCTAL_DIGITS или HEX_DIGITS. В Jimple фрагменты не используются.

Инстру­менты

Перед тем как приступить к генерации парсера, нам нужно настроить инстру­менты для работы с ANTLR. Как известно, хороший и удобный инструмент — это половина успеха. Для этого нам понадо­бится скачать ANTLR библиотеку и написать скрипты для его запуска. Существуют также maven/gradle/IntelliJ IDEA плагины, которые мы не будем исполь­зовать в этой статье, но для продук­тивной разра­ботки они могут быть полезны.

Нам понадо­бятся следующие скрипты:

Скрипт antlr4.sh

java -Xmx500M -cp ".:/usr/lib/antlr-4.12.0-complete.jar" org.antlr.v4.Tool $@

Скрипт grun.sh

java -Xmx500M -cp ".:/usr/lib/antlr-4.12.0-complete.jar" org.antlr.v4.gui.TestRig $@

Генерация парсера

Сохраните грамматику в файле Jimple.g4. Далее запустите скрипт следующим образом:

antlr4.sh Jimple.g4 -package org.jimple.lang -visitor

Параметр -package позволяет указать java package, в котором будет сгене­ри­рован код. Параметр -visitor позволяет сгене­ри­ровать интерфейс JimpleVisitor, который реализует паттерн Visitor.

После успешного выпол­нения скрипта в текущей дирек­тории появятся несколько файлов: JimpleParser.javaJimpleLexer.javaJimpleListener.javaJimpleVisitor.java. Первые два файла содержат сгене­ри­ро­ванный код парсера и лексера соответ­ственно. Остальные два файла содержат интер­фейсы для работы с деревом разбора. В этой статье мы будем исполь­зовать интерфейс JimpleVisitor, а точнее JimpleBaseVisitor — это также сгене­ри­ро­ванный класс, который реализует интерфейс JimpleVisitor и содержит реали­зации всех методов. Это позволяет переопре­делить только те методы, которые нам нужны.

Реали­зация интерпретатора

Наконец-то мы добрались до самого интересного — реали­зации интер­пре­татора. Хотя в данной статье не будем затра­гивать вопрос проверки кода на ошибки, но ошибки интер­пре­тации все-таки будут реали­зованы. Для начала, создадим класс JimpleInterpreter с методом eval, на вход которого будет подаваться строка с кодом на Jimple. Далее нам нужно разобрать исходный код на токены с помощью JimpleLexer, затем создать дерево AST, используя JimpleParser.

public class JimpleInterpreter {
    public Object eval(final String input) {
        // разбираем исходный код на токены
        final JimpleLexer lexer = new JimpleLexer(CharStreams.fromString(input));
        // создаем дерево AST
        final JimpleParser parser = new JimpleParser(new CommonTokenStream(lexer));
        // создаем объект класса JimpleInterpreterVisitor
        final JimpleInterpreterVisitor interpreterVisitor = new JimpleInterpreterVisitor(new JimpleContextImpl(stdout));
        // запускаем интерпретатор
        return interpreterVisitor.visitProgram(parser.program());
    }
}

У нас есть синтак­си­ческое дерево. Давайте добавим семантики с помощью написанного нами класса JimpleInterpreterVisitor, который будет обходить AST, вызывая соответ­ствующие методы. Так как корневым правилом нашей грамматики является правило program (см. выше program: (statement)* EOF), то обход дерева начинается именно с него. Для этого вызываем реали­зо­ванный по умолчанию метод visitProgram у объекта JimpleInterpreterVisitor, на вход которого даем объект класса ProgramContext. Реали­зация ANTLR состоит из вызова метода visitChildren(RuleNode node), который обходит все дочерние элементы заданного узла дерева, вызывая для каждого из них метод visit.

// код сгенерирован ANTLR
public class JimpleBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements JimpleVisitor<T> {
    @Override
    public T visitProgram(JimpleParser.ProgramContext ctx) {
        return visitChildren(ctx);
    }

    // другие методы опущены для краткости
}

Как можно заметить, JimpleBaseVisitor — это generic-класс, для которого нужно определить тип обработки каждого узла. В нашем случае это класс Object, так как выражения могут возвращать значения разного типа. Обычно выражение (expression) должно вернуть какое-либо значение, а утвер­ждение (statement) ничего не возвращает. В этом и заклю­чается их различие. В случае утвер­ждения можем возвращать null. Однако, чтобы случайно не столк­нуться с NullPointerException, вместо null будем возвращать объект типа Object, который глобально определен в классе JimpleInterpreter:

public static final Object VOID = new Object();

Класс JimpleInterpreterVisitor расширяет класс JimpleBaseVisitor, переопре­деляя только интере­сующие нас методы. Рассмотрим реали­зацию встро­енного оператора println, который в грамматике описан как println: 'println' expression ;. Первое, что нужно сделать — это вычислить выражение expression, для этого нам нужно вызвать метод visit и передать в него объект expression из текущего контекста PrintlnContext. В методе visitPrintln нас совер­шенно не интересует, как вычис­ляется выражение, за вычис­ление каждого правила (контекста) отвечает соответ­ствующий метод. Например, для вычис­ления строкового литерала исполь­зуется метод visitStringExpr.

public class JimpleInterpreterVisitor extends JimpleBaseVisitor<Object> {
    @Override
    public Object visitPrintln(final JimpleParser.PrintlnContext ctx) {
        final Object result = visit(ctx.expression());
        System.out.println(result);
        return null;
    }
    
    @Override
    public Object visitStringExpr(final JimpleParser.StringExprContext ctx) {
        // возвращаем строковый литерал
        return cleanStringLiteral(ctx.STRING_LITERAL().getText());
    }


    private String cleanStringLiteral(final String literal) {
        // очистить строку от кавычек
        return literal.length() > 1 ? literal.substring(1, literal.length() - 1) : literal;
    }

    // другие методы опущены для краткости
}

Реали­зовав только лишь эти методы, интер­пре­татор уже поддер­живает println и строковые литералы, что позволяет нам выполнить код println "Hello, Jimple!".

Запуск интер­пре­татора

Для запуска интер­пре­татора нужно создать стандартный метод main, который после небольших проверок, используя класс JimpleInterpreter, будет запускать наш код:

public class MainApp {
    public static void main(String[] args) {
        if (args.length < 1) {
            System.out.println("usage: jimple input.jimple");
            System.exit(1);
        }
        
        Path path = Paths.get(args[0]);
        if (!Files.exists(path)) {
            System.err.println("File not found: " + path);
            System.exit(1);
        }
        
        new JimpleInterpreter().eval(path);
    }
}

Детали реали­зации

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

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

@Override
public Object handleFunc(FunctionSignature func, List<String> parameters, List<Object> arguments, FunctionDefinitionContext ctx) {
    Map<String, Object> variables = new HashMap<>(parameters.size());
    for (int i = 0; i < parameters.size(); i++) {
        variables.put(parameters.get(i), arguments.get(i));
    }
    // создаем новую область видимости параметров функции и помещаем ее в стек
    context.pushCallScope(variables);
    // выполнение выражений функций опущены для краткости
    // удаляем область видимости параметров функции из стека
    context.popCallScope();
    return functionResult;
}

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

Зачем нужно два прохода?

Перво­на­чальная версия интер­пре­татора заклю­чалась в реали­зации метода для каждого правила. Например, если метод обработки объяв­ления функции находит функцию с таким именем (и количе­ством параметров) в текущем контексте, то выбра­сы­вается исклю­чение, иначе функция добав­ляется в текущий контекст. Таким же образом работает метод вызова функции. Если функция не найдена, то выбра­сы­вается исклю­чение, иначе функция вызывается. Такой подход работает, но он не позволяет вызывать функцию до её опреде­ления. Например, следующий код не будет работать:

var result = 9 + 10

println "Result is " + add(result, 34)

fun add (a, b){
    return a + b
}

В данном случае, у нас есть два подхода. Первый — это требовать определять функции до её исполь­зо­вания (не очень удобно для пользо­ва­телей языка). Второй — выполнять два прохода. Первый проход нужен для того, чтобы найти все функции, которые были определены в коде. А второй — непосред­ственно для выпол­нения кода. В своей реали­зации выбрал второй подход. Следует перенести реали­зацию метода visitFunctionDefinition в отдельный класс, который расширяет уже известный нам сгене­ри­ро­ванный класс JimpleBaseVisitor<T>.

// класс находит все функции в коде и регистрирует их в контексте
public class FunctionDefinitionVisitor extends JimpleBaseVisitor<Object> {
    private final JimpleContext context;
    private final FunctionCallHandler handler;

    public FunctionDefinitionVisitor(final JimpleContext context, final FunctionCallHandler handler) {
        this.context = context;
        this.handler = handler;
    }

    @Override
    public Object visitFunctionDefinition(final JimpleParser.FunctionDefinitionContext ctx) {
        final String name = ctx.name.getText();
        final List<String> parameters = ctx.IDENTIFIER().stream().skip(1).map(ParseTree::getText).toList();
        final var funcSig = new FunctionSignature(name, parameters.size());
        context.registerFunction(funcSig, (func, args) -> handler.handleFunc(func, parameters, args, ctx));
        return VOID;
    }
}

Теперь у нас есть класс, который можем исполь­зовать перед непосред­ственным запуском класса интер­пре­татора. Он будет наполнять наш контекст опреде­ле­ниями всех функций, которые будем вызывать в классе интерпретатора.

Как выглядит AST?

Для того, чтобы визуа­ли­зи­ровать AST, нужно восполь­зо­ваться утилитой grun (см. выше). Для этого следует запустить grun с параметрами Jimple program -gui (первый параметр – имя грамматики, второй – имя правила). В результате откроется окно с деревом AST. Перед выпол­нением этой утилиты важно скомпи­ли­ровать сгене­ри­ро­ванный ANTLR-ом код.

# сгенерировать парсер
antlr4.sh Jimple.g4

# скомпилировать сгенерированный код
javac -cp ".:/usr/lib/jvm/antlr-4.12.0-complete.jar" Jimple*.java

# запустить grun
grun.sh Jimple program -gui

# ввести код: "println "Hello, Jimple!""
# нажать Ctrl+D (Linux) или Ctrl+Z (Windows)

Для Jimple-кода println "Hello, Jimple!" сгене­ри­руется следующее AST:

 

Резюме

В данной статье, вы позна­ко­мились с таким понятиями как лекси­ческий и синтак­си­ческий анали­заторы. Исполь­зовали инструмент ANTLR для генерации таких анали­за­торов. Узнали, как писать грамматику ANTLR. В итоге смогли создать простой язык, а именно разра­ботали интер­пре­татор для него. В качестве бонуса смогли визуа­ли­зи­ровать AST.

Весь исходный код интер­пре­татора можно посмотреть по ссылке.

Ссылки

Действующие лица

web.sta – web-прило­жение, целевой браузер Internet Explorer версии 9 и 11
TextSystem – standalone прило­жение, написанное на Java/Swing
IntermediateLayer – Client-Server прило­жение, которое представляет собой связующее звено между web.sta и TextSystem. 

web.sta может вызвать TextSystem через IntermediateLayer. Для этого IntermediateLayer создаёт http server и web-прило­жение web.sta отправляет GET или POST запросы на localhost. Там IntermediateLayer обраба­тывает параметры запроса и стартует TextSystem. Обратной связи не требуется, поэтому схема взаимо­дей­ствия проста и доста­точно надёжна.

Citrix

В окружении Citrix мы не можем создать несколько слушающих сокетов на одном IP адресе и порте. Тут или надо менять порт, или же менять IP адрес. Но Citrix предо­ставляет специ­альный механизм для обхода этого ограни­чения, без изменения алгоритма работы программы. Этот механизм называется “Virtual IP Loopback”. Админи­стратору доста­точно указать необхо­димые прило­жения в панели конфи­гу­ри­ро­вания Citrix и прило­жение, которое использует localhost для сокетных соеди­нений, будет получать не 127.0.0.1, а IP адрес вида 127.0.0.<SID+1>, где SID это иденти­фи­катор сессии пользо­вателя Windows.

Проблема

Всё это прекрасно работало под IE9 (да и с другими браузерами тоже) на WindowsServer2008 R2. А потом клиентам захотелось чего-то нового и появился WindowsServer2012 R2 c IE11. И вся система перестала работать. Независимо от настроек Citrix IE11 всегда, при указании localhost, пытается установить соеди­нение на 127.0.0.1, а там никто не слушает. После небольшого иссле­до­вания мы пришли к выводу, что это баг именно в IE11.

RoutingService

Если вирту­а­ли­зация для localhost не работает из коробки Citrix для IE11, то давайте напишем её сами! Для этих целей мы решили написать windows service, который будет простейшим web сервером, слушать на 127.0.0.1 и перена­правлять запросы на нужные инстансы IntermediateLayer, основы­ваясь на номере сессии пользо­вателя. Простого решения получить SID мы не нашли, но в переменных окружения мы сразу обнаружили SESSIONNAME. В IE через ActiveX мы получаем переменную окружения, передаем её в качестве параметра в http запрос, в RoutingService по имени сессии мы через wtsapi32.lib получаем номер сессии. А затем перена­правляем http запрос и соответ­ственно возвращаем ответ.

Что-то пошло не так

Начали тести­ровать и внедрять наш сервис. Но не всё оказалось так радужно, как хотелось бы. Как оказалось имя сессии может меняться, правда мы так и не поняли в каких условиях это проис­ходит, а разби­раться ещё и с этим, как всегда не хватало времени. Но часто случалось, что имя сессии изменилось, а IE11 знает только перво­на­чальное значение переменной окружения. И упорно передаёт это значение на RoutingService.

А что там в реестре?

Надо найти другой способ получать sessionname. Поискали инфор­мацию о сессиях в реестре и вот что нашли: в HKEY_CURRENT_USER\Volatile Environment можно получить список сессий текущего пользователя.

Если сессия пользо­вателя одна – то вообще всё прекрасно, прочитали и используем. А если сессий одного пользо­вателя много, то надо как-то определить, в какой же мы сессии находимся. Ничего лучше, чем сопоставить путь к папке временных файлов, мы не придумали.

Вот пример:

В IE получаем текущий путь к TEMP, используя ActiveX Scripting.FileSystemObject. Таким образом нам удалось получить имя нашей сессии. Но это не всё. Значения ключей под Volatile Environment – это, собственно, SID и есть. То есть мы можем сразу в JavaScript получить необхо­димый IP адрес и оправить запрос на него.

Будем упрощать? 

В итоге мы можем получить SID и устанав­ливать соеди­нение напрямую, без исполь­зо­вания RoutingService. Но решение всё равно не выглядит красивым. Изучение интернета показало, что проблема есть, но решение этой проблемы нигде не описы­вается. А сам Microsoft молчит.

Так что, возможно, кому-то наш опыт будет полезен в решении этой специ­фичной проблемы.