10.05.2017 Установка и настройка GDAL/OGR под .NET Core

Несколько слов о проблеме и платформе .NET Core

Библиотека GDAL/OGR содержит сборки с управляемым кодом, которые возможно задействовать в своих проектах на платформе .NET Framework. Однако, сейчас набирает все большую популярность новая платформа .NET Core, которая является по настоящему кроссплатформенной, что позволяет запускать свои приложения на ОС семейства Windows, Linux и MacOS. На самом деле .NET Core – это форк .NET Framework, который был переработан с учетом требований декомпозиции. Однако, если мы попытаемся запустить проект .NET Core и выполнить все действия, как указано в статье Программируем с использованием GDAL/OGR на C# (часть 1). Установка и настройка, то получим ошибку времени компиляции: The type 'Object' is defined in an assembly that is not referenced. You must add a reference to assembly 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'. Таким образом, перефразируя, можно сказать, что для работы библиотеки GDAL/OGR нужен полноценный .NET Framework. При попытке же установки NuGet-пакета получим другую ошибку: Package restore failed. Rolling back package changes for 'ConsoleApp1'.

Установка и настройка GDAL/OGR под .NET Core

Первым делом, скачайте скомпилированные бинарники библиотеки GDAL/OGR, распакуйте их и добавьте пути к нужным директориям, как указано в статье Программируем с использованием GDAL/OGR на C# (часть 1). Установка и настройка.

Исходный код библиотеки GDAL/OGR также свободно доступен для загрузки, и мы этим воспользуемся. Заходим на официальный сайт gdal.org, переходим по ссылке Download, далее - Source и попадаем на страницу с архивами исходников. Я загрузил версию 2.1.3 (январь 2017).

Теперь для создания быстрого демонстрационного примера создадим проект консольного приложения на платформе .NET Core и назовем его GDALDemoReadNetCore. В качестве среды разработки я использую Visual Studio 2017 Community. Сразу в свойствах проекта на вкладке Build меняем целевую платформу с Any CPU на x64. Далее, в корне проекта создадим директорию GDAL и распакуем туда следующие каталоги из загруженного архива:

  • swig\csharp\const
  • swig\csharp\gdal
  • swig\csharp\ogr
  • swig\csharp\osr

Удалим из содержимого скопированных каталогов все файлы, кроме *.cs. В результате у вас должна получиться следующая структура проекта:

Структура проекта .NET Core с использованием библиотеки GDAL/OGR
Структура проекта .NET Core с использованием библиотеки GDAL/OGR

Пробуем собрать и получаем кучу ошибок. Начинаем разбираться с самой распространенной: error CS0246: The type or namespace name 'HandleRef' could not be found (are you missing a using directive or an assembly reference?). Т.е. компилятор не может найти тип HandleRef, который инкапсулирует управляемый объект, содержащий дескриптор для ресурса (передается в неуправляемый код с помощью вызова платформы). В следующих релизах .NET Core планируют добавить тип HandleRef. Ну а пока на просторах интернета я нарыл исходный код данной структуры по поисковому запросу: HandleRef source code. Исходный код имеет следующий вид:

                            // ==++== 
//
//   Copyright (c) Microsoft Corporation.  All rights reserved.
//
// ==--== 
namespace System.Runtime.InteropServices
{ 
  
    using System;
  
    [System.Runtime.InteropServices.ComVisible(true)]
    public struct HandleRef
    {
  
        // ! Do not add or rearrange fields as the EE depends on this layout.
        //------------------------------------------------------------------ 
        internal Object m_wrapper; 
        internal IntPtr m_handle;
        //----------------------------------------------------------------- 
 
 
        public HandleRef(Object wrapper, IntPtr handle)
        { 
            m_wrapper = wrapper;
            m_handle  = handle; 
        } 
 
        public Object Wrapper { 
            get {
                return m_wrapper;
            }
        } 
 
        public IntPtr Handle { 
            get { 
                return m_handle;
            } 
        }
 
 
        public static explicit operator IntPtr(HandleRef value) 
        {
            return value.m_handle; 
        } 
 
        public static IntPtr ToIntPtr(HandleRef value) 
        {
            return value.m_handle;
        }
    } 
}

Давайте включим этот фрагмент в нашу программу: создаем в каталоге GDAL нашего проекта папку Surrogate, в ней создаем файл HandleRef.cs и помещаем туда вышеуказанный код. Собираем и видим, что ошибок заметно поубавилось. Остались ошибки вида: Error CS0111. Type 'GdalPINVOKE' already defines a member called '.cctor' with the same parameter types. Во всех ошибках удаляем один лишний статический конструктор.

Далее мы сталкиваемся с рядом ошибок, которые сообщают нам о том, что не определены типы исключений SystemException и ApplicationException. Исправим это. В той же папке Surrogate создадим файл SystemException.cs и поместим туда следующий код:

namespace System
{
    public class SystemException : Exception
    {
        public SystemException() { }
        public SystemException(string message) : base(message) { }
        public SystemException(string message, Exception innerException) : base(message, innerException) { }
    }
}

Аналогично в папке Surrogate создадим файл ApplicationException.cs и поместим туда похожий код:

namespace System
{
    public class ApplicationException : Exception
    {
        public ApplicationException() { }
        public ApplicationException(string message) : base(message) { }
        public ApplicationException(string message, Exception innerException) : base(message, innerException) { }
    }
}

Пробуем собрать и получаем оставшиеся три ошибки:

  • Error CS0117. 'GdalPINVOKE' does not contain a definition for 'BandUpcast'
  • Error CS0117. 'GdalPINVOKE' does not contain a definition for 'DriverUpcast'
  • Error CS0117. 'GdalPINVOKE' does not contain a definition for 'DatasetUpcast'

Если открыть исходник GdalPINVOKE.cs, то можно обнаружить, что среди импортируемых функций из файла gdal_wrap.dll присутствуют чем-то похожие, но нигде не используемые функции:

  • public static extern IntPtr Band_SWIGUpcast(IntPtr jarg1);
  • public static extern IntPtr Driver_SWIGUpcast(IntPtr jarg1);
  • public static extern IntPtr Dataset_SWIGUpcast(IntPtr jarg1);

Поэтому давайте их переименуем:

  • public static extern IntPtr BandUpcast(IntPtr jarg1);
  • public static extern IntPtr DriverUpcast(IntPtr jarg1);
  • public static extern IntPtr DatasetUpcast(IntPtr jarg1);

Теперь давайте проверим, существуют ли функции, указанные в атрибуте DllImport в качестве точки входа (EntryPoint) в нативной динамически подключаемой библиотеке gdal_wrap.dll. Для этого я воспользуюсь утилитой Dependency Walker 2.2. Спустя некоторое время можно заметить, что данные функции имеют следующий вид: CSharp_XXXUpcast, поэтому меняем указанное имя функции в атрибуте DllImport на правильное. В результате эти 3 импортируемые функции будут выглядеть следующим образом (файл GdalPINVOKE.cs):

  [DllImport("gdal_wrap", EntryPoint="CSharp_DriverUpcast")]
  public static extern IntPtr DriverUpcast(IntPtr jarg1);

  [DllImport("gdal_wrap", EntryPoint="CSharp_DatasetUpcast")]
  public static extern IntPtr DatasetUpcast(IntPtr jarg1);

  [DllImport("gdal_wrap", EntryPoint="CSharp_BandUpcast")]
  public static extern IntPtr BandUpcast(IntPtr jarg1);

Теперь можно пересобрать проект и убедиться, что сборка проходит без ошибок. Чтобы проверить работу библиотеки я воспользуюсь кодом программы из статьи Программируем с использованием GDAL/OGR на C# (часть 2). Получение пространственных данных. Для удобства я приведу фрагмент кода ниже:

using OSGeo.OGR;
using System;

namespace GDALDemoRead
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length < 2)
            {
                Console.WriteLine("Usage: GDALDemoReadNetCore <datasource> <layername>");
                PauseAndExit(0);
            }

            Ogr.RegisterAll();

            int errCode = 0;
            using (DataSource ds = Ogr.Open(args[0], 0))
            {
                if (ds == null)
                {
                    Console.WriteLine("Open failed");
                    errCode = 1;
                }

                if (errCode == 0)
                {

                    Layer layer = ds.GetLayerByName(args[1]);
                    if (layer == null)
                    {
                        Console.WriteLine("Layer not found");
                        errCode = 2;
                    }

                    if (errCode == 0)
                    {
                        int n = 1;
                        Feature feature;
                        while ((feature = layer.GetNextFeature()) != null)
                        {
                            Console.WriteLine($"Feature #{n}");
                            PrintAttributes(feature);
                            PrintGeometry(feature);
                            n++;
                        }
                    }
                }
            }

            PauseAndExit(0);
        }

        static void PauseAndExit(int errCode)
        {
            Console.ReadLine();
            Environment.Exit(errCode);
        }

        static void PrintAttributes(Feature feature)
        {
            ConsoleColor oldColor = Console.ForegroundColor;
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("  Attributes:");
            Console.ForegroundColor = oldColor;
            int fieldsCount = feature.GetFieldCount();
            for (int i = 0; i < fieldsCount; i++)
            {
                object val = null;
                string fName = feature.GetFieldDefnRef(i).GetName();
                FieldType fType = feature.GetFieldType(i);
                switch (fType)
                {
                    case FieldType.OFTString:
                        val = feature.GetFieldAsString(i);
                        break;
                    case FieldType.OFTInteger:
                        val = feature.GetFieldAsInteger(i);
                        break;
                    case FieldType.OFTInteger64:
                        val = feature.GetFieldAsInteger64(i);
                        break;
                    case FieldType.OFTReal:
                        val = feature.GetFieldAsDouble(i);
                        break;
                    default:
                        val = "<unknown>";
                        break;
                }
                Console.WriteLine($"  Name:{fName}, Value:{val}");
            }
        }

        static void PrintGeometry(Feature feature)
        {
            ConsoleColor oldColor = Console.ForegroundColor;
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("  Geometry:");
            Console.ForegroundColor = oldColor;
            PrintGeometry(feature.GetGeometryRef(), "  ");
        }

        static void PrintGeometry(Geometry geom, string padding)
        {
            Console.WriteLine($"{padding}GeometryName:{geom.GetGeometryName()}, GeometryType:{geom.GetGeometryType()}");
            int pointCount = geom.GetPointCount();
            for (int j = 0; j < pointCount; j++)
            {
                double[] points = new double[3];
                geom.GetPoint(j, points);
                Console.WriteLine($"{padding}x:{points[0]}, y:{points[1]}");
            }

            int geomCount = geom.GetGeometryCount();
            for (int i = 0; i < geomCount; i++)
            {
                PrintGeometry(geom.GetGeometryRef(i), padding + "  ");
            }
        }
    }
}

Итак, заменим содержимое файла Program.cs фрагментом кода, приведенным выше. Пересобираем и запускаем программу:

dotnet.exe GDALDemoReadNetCore.dll "MSSQL:server=.\SQLEXPRESS;uid=sa;pwd=12345;database=MySpatialDb;Integrated Security=false;tables=tm_world_borders_simpl(ogr_geometry)" tm_world_borders_simpl

Все отрабатывает отлично! Конечно, данный способ установки GDAL/OGR для работы под .NET Core можно считать достаточно "костылеобразным", и нам, разработчикам, ГИС и веб-ГИС приложений, остается надеяться, что в скором будущем будет добавлена и обертка для платформы .NET Core от разработчиков этой библиотеки.

Для всех желающих посмотреть вживую процесс установки, я снял небольшой видеоролик:


Также вы можете загрузить с github исходники проекта GDALDemoReadNetCore.