Ответ 1
Я расскажу здесь о своем комментарии к сообщению FUZxxl.
Приведенные вами примеры возможны с помощью FFI
. Как только вы экспортируете свои функции с помощью FFI, вы можете, как вы уже разобрались, скомпилировать программу в DLL.
.NET был разработан с целью легко взаимодействовать с C, С++, COM и т.д. Это означает, что как только вы сможете скомпилировать свои функции в DLL, вы можете назвать это (относительно) легко из.СЕТЬ. Как я уже упоминал ранее в своем другом сообщении, с которым вы связались, помните, какое соглашение о вызове вы указываете при экспорте своих функций. Стандартом в .NET является stdcall
, в то время как (наиболее) примеры экспорта Haskell FFI
с использованием ccall
.
До сих пор единственным ограничением, которое я нашел в том, что может быть экспортировано FFI, является polymorphic types
или типы, которые не применяются полностью. например ничего, кроме вида *
(вы не можете экспортировать Maybe
, но вы можете экспортировать Maybe Int
, например).
Я написал инструмент Hs2lib, который будет автоматически охватывать и экспортировать любую из функций, которые у вас есть в вашем примере. Он также имеет возможность генерировать код unsafe
С#, который делает его в значительной степени "подключи и играй". Причина, по которой я выбрал небезопасный код, заключается в том, что с ними проще обрабатывать указатели, что в свою очередь упрощает процесс сортировки для данных.
Чтобы быть полным, я подробно расскажу, как инструмент обрабатывает ваши примеры и как я планирую обработку полиморфных типов.
- Функции более высокого порядка
При экспорте функций более высокого порядка функцию нужно слегка изменить. Аргументы более высокого порядка должны стать элементами FunPtr. В основном они рассматриваются как явные указатели функций (или делегаты в С#), а именно то, как более высокая упорядоченность обычно выполняется на императивных языках.
Предполагая, что мы преобразуем Int
в CInt
, тип double преобразуется из
(Int -> Int) -> Int -> Int
в
FunPtr (CInt -> CInt) -> CInt -> IO CInt
Эти типы генерируются для функции-обертки (doubleA
в этом случае), которая экспортируется вместо double
. Функции обертки отображаются между экспортируемыми значениями и ожидаемыми входными значениями для исходной функции. IO необходим, потому что построение a FunPtr
не является чистой операцией.
Следует помнить, что единственный способ построить или разыменовать FunPtr
- это статическое создание импорта, которое инструктирует GHC создавать для этого заглушки.
foreign import stdcall "wrapper" mkFunPtr :: (Cint -> CInt) -> IO (FunPtr (CInt -> CInt))
foreign import stdcall "dynamic" dynFunPtr :: FunPtr (CInt -> CInt) -> CInt -> CInt
Функция "обертка позволяет нам создать FunPtr
и " динамический " FunPtr
, позволяющий поклоняться одному.
В С# мы объявляем ввод как IntPtr
, а затем используем вспомогательную функцию Marshaller
Marshal.GetDelegateForFunctionPointer для создания указателя функции, который мы можем вызвать, или обратную функцию для создания IntPtr
из указателя функции.
Также помните, что вызывающее соглашение функции, передаваемой как аргумент FunPtr, должно соответствовать вызывающему соглашению функции, к которой передается аргумент. Другими словами, передача &foo
в bar
требует, чтобы foo
и bar
имели одно и то же соглашение о вызовах.
- Пользовательские типы данных
Экспорт пользовательского типа данных на самом деле довольно прямолинейный. Для каждого типа данных, который необходимо экспортировать, для этого типа должен быть создан экземпляр Storable. В этих экземплярах указывается информация для сортировки, которую требуется GHC, чтобы иметь возможность экспортировать/импортировать этот тип. Помимо прочего, вам нужно будет определить типы size
и alignment
типа, а также как читать/записывать указателю значения типа. Я частично использую Hsc2hs для этой задачи (следовательно, макросы C в файле).
newtypes
или datatypes
с помощью только конструктора один. Они становятся плоской структурой, поскольку существует только одна возможная альтернатива при построении/разрушении этих типов. Типы с несколькими конструкторами становятся объединением (структура с атрибутом Layout
, установленным на Explicit
в С#). Однако нам также необходимо включить перечисление, чтобы определить, какая конструкция используется.
в общем случае тип данных Single
, определенный как
data Single = Single { sint :: Int
, schar :: Char
}
создает следующий Storable
экземпляр
instance Storable Single where
sizeOf _ = 8
alignment _ = #alignment Single_t
poke ptr (Single a1 a2) = do
a1x <- toNative a1 :: IO CInt
(#poke Single_t, sint) ptr a1x
a2x <- toNative a2 :: IO CWchar
(#poke Single_t, schar) ptr a2x
peek ptr = do
a1' <- (#peek Single_t, sint) ptr :: IO CInt
a2' <- (#peek Single_t, schar) ptr :: IO CWchar
x1 <- fromNative a1' :: IO Int
x2 <- fromNative a2' :: IO Char
return $ Single x1 x2
и C struct
typedef struct Single Single_t;
struct Single {
int sint;
wchar_t schar;
} ;
Функция foo :: Int -> Single
будет экспортироваться как foo :: CInt -> Ptr Single
Хотя тип данных с несколькими конструкторами
data Multi = Demi { mints :: [Int]
, mstring :: String
}
| Semi { semi :: [Single]
}
генерирует следующий код C:
enum ListMulti {cMultiDemi, cMultiSemi};
typedef struct Multi Multi_t;
typedef struct Demi Demi_t;
typedef struct Semi Semi_t;
struct Multi {
enum ListMulti tag;
union MultiUnion* elt;
} ;
struct Demi {
int* mints;
int mints_Size;
wchar_t* mstring;
} ;
struct Semi {
Single_t** semi;
int semi_Size;
} ;
union MultiUnion {
struct Demi var_Demi;
struct Semi var_Semi;
} ;
Экземпляр Storable
является относительно прямым и должен легче следовать из определения структуры C.
- Применяемые типы
Мой трассировщик зависимостей будет испускать для типа Maybe Int
зависимость от типа Int
и Maybe
. Это означает, что при генерации экземпляра Storable
для Maybe Int
голова выглядит как
instance Storable Int => Storable (Maybe Int) where
То есть, поскольку существует экземпляр Storable для аргументов приложения, сам тип также может быть экспортирован.
Так как Maybe a
определяется как имеющий полиморфный аргумент Just a
, при создании структур теряется некоторая информация о типе. Структуры будут содержать аргумент void*
, который вы должны вручную преобразовать в нужный тип. Альтернатива была слишком громоздкой, на мой взгляд, которая также заключалась в создании специализированных структур. Например. struct MaybeInt. Но количество специализированных структур, которые могут быть созданы из нормального модуля, может быстро взорваться таким образом. (может добавить это как флаг позже).
Чтобы облегчить эту потерю информации, мой инструмент будет экспортировать любую документацию Haddock
, найденную для функции, поскольку комментарии в сгенерированном включении. Он также поместит оригинальную подпись типа Haskell в комментарий. Затем IDE представит их как часть своего Intellisense (компиляция кода).
Как и во всех этих примерах, я обошел код для сторон .NET. Если вас интересует, что вы можете просто просмотреть вывод Hs2lib.
Существует несколько других типов, требующих специального лечения. В частности, Lists
и Tuples
.
- В списках необходимо передать размер массива, из которого нужно вывести маршалл, поскольку мы взаимодействуем с неуправляемыми языками, где размер массивов неявно известен. Обратно, когда мы возвращаем список, нам также нужно вернуть размер списка.
-
Кортежи - это специальная сборка в типах. Чтобы экспортировать их, мы должны сначала сопоставить их с "нормальным" типом данных и экспортировать их. В инструменте это делается до 8-ти кортежей.
- Полиморфные типы
Проблема с полиморфными типами e.g. map :: (a -> b) -> [a] -> [b]
заключается в том, что size
из a
и b
не знают. То есть, нет возможности зарезервировать пространство для аргументов и вернуть значение, поскольку мы не знаем, что это такое. Я планирую поддержать это, разрешив вам указать возможные значения для a
и b
и создать специализированную функцию-обертку для этих типов. В другом размере на императивном языке я бы использовал overloading
, чтобы представить типы, которые вы выбрали для пользователя.
Что касается классов, предположение о открытом мире Haskell обычно является проблемой (например, экземпляр может быть добавлен в любое время). Однако во время компиляции доступен только статически известный список экземпляров. Я намерен предложить вариант, который автоматически экспортирует как можно больше специализированных экземпляров, используя этот список. например export (+)
экспортирует специализированную функцию для всех известных экземпляров Num
во время компиляции (например, Int
, double
и т.д.).
Инструмент также довольно доверчив. Поскольку я не могу действительно проверить код для чистоты, я всегда верю, что программист честен. Например. вы не передаете функцию, которая имеет побочные эффекты для функции, ожидающей чистой функции. Будьте честны и отмечайте, что более высокий порядок аргументов является нечистым, чтобы избежать проблем.
Надеюсь, это поможет, и я надеюсь, что это было не слишком долго.
Обновление. Там была какая-то большая проблема, которую я недавно обнаружил. Мы должны помнить, что тип String в .NET является неизменным. Поэтому, когда маршаллер отправляет его с кодом Haskell, мы получаем CWString копию оригинала. У нас есть, чтобы освободить это. Когда GC выполняется в С#, это не повлияет на CWString, которая является копией.
Однако проблема заключается в том, что когда мы освобождаем ее в коде Haskell, мы не можем использовать freeCWString. Указатель не был выделен с помощью C (msvcrt.dll) alloc. Есть три способа (которые я знаю), чтобы решить эту проблему.
- используйте char * в коде С# вместо String при вызове функции Haskell. Затем у вас есть указатель на свободный, когда вы вызываете return, или инициализируете функцию с помощью fixed.
- импортировать CoTaskMemFree в Haskell и освободить указатель в Haskell
- используйте StringBuilder вместо String. Я не совсем уверен в этом, но идея в том, что, поскольку StringBuilder реализован как собственный указатель, Marshaller просто передает этот указатель на ваш код Haskell (который также может его обновить). Когда GC выполняется после возврата вызова, StringBuilder должен быть освобожден.