Очевидный способ собрать решение, написанное на нескольких языках программирования .NET - это код на каждом языке оформить отдельной сборкой (assembly) в виде dll, а затем подключить их к исполняемому exe файлу основной программы. Этот способ предлагается Visual Studio - к другим она не приспособлена. Не всегда удобно создавать решение из множества файлов, и средства самого .NET позволяют справиться с этой задачей. Главная сложность состоит в том, чтобы и сохранить удобную среду разработки Visual Studio, и получить желаемое решение.
Для начала создадим инфраструктуру.
1) Создаём пустой solution: в системном меню выбираем File/New Project. В открывшем диалоге выбираем Other Project Types/Visual Studio Solutions\Blank Solution. Задаём имя, допустим, mulilang. Создаём solution.
2) Добавляем в solution пустой проект C#: правой кнопкой мыши кликаем на Solution и в контекстном меню выбираем Add/New Project. В открывшемся диалоге выбираем Visual C#/Windows/Empty Project. Задаём имя, допустим, csl. Создаём проект.
3) Изменяем тип проекта csl на нестандартный. Для этого:
3.1) Правой кнопкой мыши кликаем на проект csl и в контекстном меню выбираем Unload Project. Затем опять правкой кнопкой мыши кликаем на проект csl и в контекстном меню выбираем Edit.
3.2) В открывшемся XML-файле проекта находим элемент Project/PropertyGroup/OutputType. Стандартным редактором свойств туда можно установить только три значения: "Exe" (Console Application), генерирующий файл типа exe, запускающийся в чёрном окне консоли, "Winexe" (Windows Application), генерирующий файл типа exe, запускающийся без окна консоли, и "Library" (Class Library), генерирующий файл типа dll, пригодный к подключению к другим проектам типа exe и dll.
Есть недокументированное значение: "Module", генерирующее файл типа netmodule. Этот файл подобен библиотеке dll и может подключаться к другим exe и dll. Разница заключается в том, что любой exe и dll представляет собой законченную сборку (assembly), а netmodule - только составную часть какой-то сборки. Т.е. при помощи модулей в .NET можно создавать многофайловые сборки. Например, такие, части которых написаны на разных языках программирования.
И вот нам нужно в XML-узле Project/PropertyGroup/OutputType записать значение Module, заставящее комплиятор C# сгенерировать файл модуля.
3.3) Поменяв значение, сохраняем файл проекта. Обязательно закрываем окно редактора. Правой кнопкой мыши кликаем на проект csl и в контекстном меню выбираем Reload Project.
Теперь, если открыть свойства проекта, мы увидим там на вкладке Application в выпадающем списке Output Type какое-то одно из стандартных значений, что неправильно, но на самом деле, если не трогать этот выпадающий список, там сохранится значение Module. Остальные свойства проекта можно менять свободно и даже конвертировать solution в новые версии Visual Studio - значение не испортится.
4) Наполним проект содержанием. Правой кнопкой мыши кликаем на проект csl и выбираем в контекстном меню пункт Add/New Item. В открывшемся диалоге выбираем Class. Зададим имя, допустим, CSL. Добавляем класс в проект. Оформим его как точку входа в приложение: добавим метод Main и какой-нибудь вывод на экран.
using System;
namespace csl
{
class CSL
{
static void Main(string[] argc)
{
Console.WriteLine("Hello world.");
Console.ReadKey();
}
}
}
5) Делам Build для solution и убеждаемся, что всё нормально. Ищем на диске наш solution, открываем папку multilang/csl/bin/<Имя конфигурации> (Debug или Release) и смотрим, что там появился файл csl.netmodule. Этот файл не является исполняемым.
6) Чтобы наша программа заработала, нужно полученный модуль подключить к какому-нибудь exe. При использовании редактора связей .NET al (который вызывают компиляторы C#, VB и других языков .NET) можно построить только такие exe и dll, у которых модули остаются внешними. Т.е. для запуска программы рядом с exe-файлом должны находится файлы netmodule. Это нам не интересно. А вот редактор связей C++ link умеет собирать такие exe, внутрь которых встраиваются netmodule. Этим мы и воспользуемся.
7) Добавим в solution пустой проект C++: правой кнопкой мыши кликаем на multilang и в контекстном меню выбираем Add/New Project. В открывшемся диалоге выбираем Visual C++/General/Empty Project. Задаём имя, допустим, app. Создаём проект.
7.1) Этот проект будет у нас главным для запуска, поэтому правой кнопкой мыши кликаем на app и в контекстном меню выбираем Set as StartUp Project.
8 ) Поскольку нас интересует только редактор связей, никакого полезного содержания в проекте app у нас не будет. Но чтобы процесс сборки вообще запускался, мы добавим один пустой файл C++. Кликаем правой кнопкой мыши на проект app, в контекстном меню выбираем пункт Add/New Item. В открывшемся диалоге выбираем C++ File. Зададим имя, допустим, stub. Добавляем файл в проект.
9) Сборка проектов C++ отличается от сборок проектов других языков местом расположения файлов результата. Они будут попадать в папку multilang/<Имя конфигурации> - т.е. находятся на уровне solution, а не отдельного проекта.
Чтобы наш модуль попадал именно туда, нужно изменить свойства проекта csl. Кликаем правой кнопкой мыши на проект csl и выбираем в контекстном меню пункт Properties. Находим на вкладке Build элемент Output path. Если текущая конфигурация Debug, там написано "bin\Debug\". Меняем на "..\Debug\". Сверху в выпадающем списке Configuration выбираем следующую конфигурацию Release и в ней тоже меняем значение на "..\Release\". Возвращаем исходную конфигурацию, сохраняем и закрываем окно свойств проекта csl.
10) Связываем проекты зависимостью: кликаем правой кнопкой мыши на проекте app и в контекстном меню выбираем Project Dependencies. В открывшемся диалоге ставим галочку на проекте csl и закрываем окно.
11) Настраиваем редактор связей: кликаем правой кнопкой мыши на проекте app и в котекстном меню выбираем Properties. В открывшемся окне сверху в выпадающем списке Configuration выбираем All configurations.
11.1) В дереве выбираем элемент Configuration Properties/Linker/Input. В поле Additional Dependencies записываем значение (с кавычками) "$(OutDir)\csl.netmodule".
11.2) В дереве выбираем элемент Configuration Properties/Linker/System. В поле SubSystem выбираем значение Console. (Это для нашего случая, но в остальных случаях какое-то конкретное значение этой настройки нужно указывать обязательно.)
11.3) В дереве выбираем элемент Configuration Properties/Linker/Advanced. В поле Entry Point записываем значение "csl.CSL.Main" - полное имя метода Main точки входа в программу (см. код выше).
Закрываем окно свойств.
12) Надо проверить результат. Нажимаем зелёную стрелку запуска или кнопку F5. Всё должно собраться, программа должна запуститься, и в окне консоли появится надпись "Hello world.". Нажатием любой клавиши можем завершить работу нашей программы. При желании можно скопировать файл multilang/<Имя конфигурации>/app.exe в другое место, где нет файлов netmodule, и убедиться, что и там наша программа успешно запускается.
Итак, мы получили инфраструктуру для многоязыковых сборок. Основная её идея заключается в том, что отдельные блоки кода оформляются не в виде Class library, а в виде Module при помощи описанной выше подмены свойств проектов.
Полученные файлы netmodule с их полными путями необходимо добавить в Linker/Input/Additional Dependencies через пробел.
Другой вопрос, как организовать сборку модулей, ссылающихся друг на друга, поскольку воспользоваться стандартным механизмом ссылок (References) в свойствах проекта мы не можем - для ссылок нужны законченные сборки (dll, exe), а не модули.
К примеру, создадим модуль на языке VB.NET, который мы хотим вызывать из нашей программы на C#.
1) Добавим в solution пустой проект VB.NET и меняем его тип, как это описано выше в пунктах 2-3 предыдущего раздела. Назовём проект, допустим, vbl.
2) Отредактируем пути выходных файлов проекта vbl и привяжем его к проекту app, как это описано в пунктах 9-10 предыдущего раздела (с той лишь разницей, что вкладке Build в свойствах проекта C# соответствует вкладка Compile в свойствах проекта VB.NET). Также укажем файл "$(OutDir)\vbl.netmodule" в качестве входного для редактора связей проекта app (см. пункт 11.1 предыдущего раздела). Не следует забывать делать изменения свойств для All configurations, иначе возникнет разница между Debug и Release сборками.
3) Добавим класс Hello в проект vbl (аналогично пункту 4 предыдущего раздела) и в нём статический метод Hello для вывода какой-нибудь надписи на экран:
Public Class Hello
Public Shared Sub Hello()
Console.WriteLine("Hello VB.NET")
End Sub
End Class
4) Соберём solution и убедимся, что всё хорошо собирается.
5) Теперь перед нами стоит задача вызвать vbl.Hello.Hello из метода csl.Program.Main. Для этого нужно связать проекты модулей, и это придётся сделать вручную.
5.1) Выгружаем проект csl и открываем его XML-файл редактором (см. пункт 3.1 предыдущего раздела). Находим в XML-файле секции Project/ItemGroup. Добавляем собственную секцию в элемент Project:
<ItemGroup>
<AddModules Include="$(OutDir)\vbl.netmodule" />
</ItemGroup>
Редактор может показывать, что такой элемент некорректен для текущей схемы XML - не обращаем на это внимание. Сохраняем изменения и загружаем проект csl обратно (см. пункт 3.3 предыдущего раздела). Если модуль vbl.netmodule был успешно собран и находится в папке с прочими выходными файлами, он будет показан в дереве проекта csl пустой иконкой. Если файла модуля нет, то иконка будет преупреждающей жёлтой с вопросительным знаком.
5.2) Для предотвращения ошибок нужно задать правильный порядок сборки проектов в solution. Он устанавливается автоматически по зависимостям между проектами. Но поскольку в нашем случае зависимость нестандартная, Visual Studio не сможет определить порядок, и его следует указать вручную. Кликаем правой кнопкой мыши на multilang и выбираем в контекстном меню Project Build Order. В открывшемся окне на вкладке Dependencies выбираем проект csl в выпадающем списке Projects и в перечне ставим галочку на проекте vbl, тем самым вручную устанавливая зависимость.
5.3) Добавляем взаимодействие модулей. В проекте csl вставляем в нашу программу вызов нового модуля:
static void Main(string[] argc)
{
vbl.Hello.Hello();
Console.WriteLine("Hello C#.NET");
Console.ReadKey();
}
Редактор покажет, что пространство имён vbl и класс Hello ему неизвестны, не распознает всю строчку и не будет показывать подсказки при наборе кода. Тем не менее код правильный. Такова цена за использование недокументированных возможностей.
6) Собираем solution и запускаем программу - убеждаемся, что всё работает.
Hello VB.NET
Hello C#.NET
P.S. С самым интересным - добавлением C++ - всё не так гладко, об этом позже.