Использование dotless из C# и особенности компиляции под CLI
В конце прошлого года я написал пост про dotless, в котором я описал один из способов использования этой библиотеки.
Только что товарищ Elayven попросил помочь ему и сконвертировать код из этого поста на C#. Ну что ж, без проблем, код-то ведь там простейший, и ничего F#-специфичного в нём нету, правильно?
Оказывается, нет, неправильно.
Посмотрим внимательно на пару строчек из этого кода:
open dotless.Core
open dotless.Core.configuration
let config = DotlessConfiguration()
let locator = ContainerFactory().GetContainer(config)
let engine = (locator.GetService (typeof<ILessEngine>)) :?> ILessEngine
Здесь мы видим несколько вызовов конструкторов и методов. Казалось бы, что может пойти не так? Хорошо, перепишем код на C# (создав новое консольное приложение и подключив туда библиотеку dotless):
using dotless.Core;
using dotless.Core.configuration;
namespace Dotless.Console
{
class Program
{
static void Main()
{
var config = new DotlessConfiguration();
var locator = new ContainerFactory().GetContainer(config);
var engine = (ILessEngine) locator.GetService(typeof(ILessEngine));
}
}
}
Этот код не скомпилируется, и заругается на вызов метода GetContainer(config)
вот таким вот благим матом:
error CS0122: 'ContainerFactory.GetContainer(DotlessConfiguration)' is inaccessible due to its protection level
Я набирал код в Visual Studio, и она вообще-то не должна бы мне показывать недоступные методы, так что я полез в исходники (прямиком из декомпилятора, чтоб уменьшить вероятность того, что я смотрю исходники не от той версии библиотеки). И что же, в исходниках чёрным по белому написано:
namespace dotless.Core
{
public class ContainerFactory
{
// ...
public IServiceLocator GetContainer(DotlessConfiguration configuration)
{
// ...
}
// ...
}
}
Класс и метод — публично доступные, проблем быть не должно. Какое-то время я
провозился с ildasm
, почитал код библиотеки на GitHub, но не
увидел причины такому странному поведению. Однако же, стоило мне перейти в
декомпилированную версию интерфейса IServiceLocator
— и я увидел следующее:
using System;
using System.Collections.Generic;
namespace Microsoft.Practices.ServiceLocation
{
internal interface IServiceLocator : IServiceProvider
{
object GetInstance(Type serviceType);
object GetInstance(Type serviceType, string key);
IEnumerable<object> GetAllInstances(Type serviceType);
TService GetInstance<TService>();
TService GetInstance<TService>(string key);
IEnumerable<TService> GetAllInstances<TService>();
}
}
Постойте, что же это получается: internal
-интерфейс использован в
public
-методе? Так вообще бывает?
На этом месте я уже поискал IServiceLocator
в багтрекере библиотеки и
наткнулся на вот это интересное обсуждение. Окончательные бинарники
dotless формируются с помощью ILMerge, который помечает общедоступные элементы
присоединяемых библиотек как internal
. В обсуждении я вычитал альтернативный
вариант решения моей проблемы без обращения к этому не вполне поддерживаемому
API. Код решения я залил в отдельный репозиторий на GitHub.
Какие же выводы мы можем сделать из всего перечисленного?
Начнём с того, что в обычной ситуации компилятор C# не позволяет создавать такие сборки, как dotless. Использовать внутренний тип в публичном API не получится. Например, такой код не скомпилируется:
internal class Internal {}
public class Public {
public void Foo(Internal i) {} // error CS0051: Inconsistent accessibility
}
В то же время, ILMerge, очевидно, без проблем создаёт такие сборки.
При этом компилятор C# не умеет вообще ссылаться на внутренние типы,
используемые в публичном API. Поэтому он не позволяет мне обратиться к
(публичному) методу, который возвращает объект internal
-типа.
Компилятор F#, очевидно, без проблем на этот тип ссылается (аллоцирует локальную переменную этого типа и позволяет даже вызвать у неё методы).
Кто же прав? Валиден ли такой код в CLI? Нас может рассудить только
спецификация. Открываем настольную книгу каждого .NET-программиста — пятую редакцию ECMA-335:
Common Language Infrastructure (CLI) — которая, к счастью, бесплатно
имеется в публичном доступе (увы, в отличие от стандартов некоторых других
языков программирования). Нас интересует раздел 8.5.3, который достаточно
ясно говорит, что типы, объявленные со спецификатором internal
(или private
в терминологии CIL) снаружи сборки использовать нельзя. Попробую отправить баг
разработчикам компилятора F# — выходит, что он в таких случаях генерирует код,
не совместимый с моделью CIL (который, хоть и выполняется на Full CLR, но легко
может отвалиться на .NET Core или Mono — такие случаи уже бывали).