2011-07-03 31 views
27

Làm thế nào tôi có thể sử dụng và gọi chức năng Haskell với kiểu bậc cao chữ ký từ C# (DllImport), giống như ...Sử dụng bậc cao loại Haskell trong C#

double :: (Int -> Int) -> Int -> Int -- higher order function 

typeClassFunc :: ... -> Maybe Int -- type classes 

data MyData = Foo | Bar    -- user data type 
dataFunc :: ... -> MyData 

loại chữ ký tương ứng trong C# là gì ?

[DllImport ("libHSDLLTest")] 
private static extern ??? foo(???); 

Thêm vào đó (vì nó có thể được dễ dàng hơn): Làm thế nào tôi có thể sử dụng "không rõ" loại Haskell trong C#, vì vậy tôi ít nhất có thể vượt qua chúng xung quanh, mà không cần C# không biết bất kỳ loại hình cụ thể? Chức năng quan trọng nhất mà tôi cần phải biết là vượt qua một loại lớp (như Monad hoặc Arrow).

Tôi đã biết how to compile a Haskell library to DLL và sử dụng trong C#, nhưng chỉ cho các chức năng đặt hàng đầu tiên. Tôi cũng biết về Stackoverflow - Call a Haskell function in .NET, Why isn't GHC available for .NEThs-dotnet, nơi tôi không tìm thấy bất kỳ tài liệu và mẫu nào (theo hướng C# đến Haskell).

Trả lời

18

Tôi sẽ giải thích ở đây trên nhận xét của tôi về bài đăng của FUZxxl.
Các ví dụ bạn đã đăng đều có thể sử dụng FFI. Một khi bạn xuất khẩu các chức năng của bạn bằng cách sử dụng FFI bạn có thể như bạn đã tìm ra biên dịch chương trình vào một DLL. .NET được thiết kế với mục đích có thể dễ dàng giao tiếp với C, C++, COM, v.v. Điều này có nghĩa là khi bạn có thể biên dịch các hàm của mình thành một DLL, bạn có thể gọi nó (tương đối) dễ dàng từ .NET. Như tôi đã đề cập trước đó trong bài viết khác của tôi mà bạn đã liên kết, hãy nhớ quy ước gọi bạn chỉ định khi xuất các hàm của bạn. Tiêu chuẩn trong .NET là stdcall, trong khi (hầu hết) ví dụ về xuất khẩu Haskell FFI bằng cách sử dụng ccall.

Cho đến nay, giới hạn duy nhất tôi đã tìm thấy trên những gì có thể được xuất bởi FFI là polymorphic types hoặc các loại không được áp dụng đầy đủ. ví dụ. bất kỳ điều gì khác ngoài loại * (Bạn không thể xuất Maybe nhưng bạn có thể xuất ví dụ Maybe Int).

Tôi đã viết một công cụ Hs2lib có thể bao gồm và xuất tự động bất kỳ chức năng nào bạn có trong ví dụ của mình. Nó cũng có tùy chọn tạo ra unsafe mã C# khiến nó hoạt động khá nhiều "plug and play". Lý do tôi đã chọn mã không an toàn là bởi vì nó dễ dàng hơn để xử lý con trỏ với, do đó làm cho nó dễ dàng hơn để làm các marshalling cho datastructures.

Để hoàn thành, tôi sẽ trình bày chi tiết cách công cụ xử lý các ví dụ của bạn và cách tôi lên kế hoạch xử lý các loại đa hình.

  • cao chức năng để

Khi xuất khẩu chức năng bậc cao, các chức năng cần phải thay đổi một chút. Đối số bậc cao cần phải trở thành các phần tử của FunPtr. Về cơ bản, chúng được coi là các con trỏ hàm rõ ràng (hoặc các đại biểu trong C#), đó là thứ tự cao hơn thường được thực hiện bằng các ngôn ngữ mệnh lệnh.
Giả sử chúng tôi chuyển đổi Int vào CInt loại kép là chuyển đổi từ

(Int -> Int) -> Int -> Int 

vào

FunPtr (CInt -> CInt) -> CInt -> IO CInt 

Những loại được tạo ra cho một chức năng wrapper (doubleA trong trường hợp này) mà được xuất khẩu thay vì double chinh no. Các hàm bao bọc ánh xạ giữa các giá trị được xuất và các giá trị đầu vào được mong đợi cho hàm ban đầu. IO là cần thiết vì việc xây dựng một FunPtr không phải là một hoạt động thuần túy.
Một điều cần nhớ là cách duy nhất để xây dựng hoặc dereference là FunPtr là bằng cách tạo các nhập khẩu tĩnh, hướng dẫn GHC tạo ra sơ khai cho việc này.

foreign import stdcall "wrapper" mkFunPtr :: (Cint -> CInt) -> IO (FunPtr (CInt -> CInt)) 
foreign import stdcall "dynamic" dynFunPtr :: FunPtr (CInt -> CInt) -> CInt -> CInt 

Các "wrapper" chức năng cho phép chúng ta tạo ra một FunPtr"năng động"FunPtr cho phép một để tôn kính một.

Trong C# chúng ta khai báo đầu vào như một IntPtr và sau đó sử dụng Marshaller helper chức năng Marshal.GetDelegateForFunctionPointer để tạo ra một con trỏ hàm mà chúng ta có thể gọi điện thoại, hoặc chức năng nghịch đảo để tạo ra một IntPtr từ một con trỏ hàm.

Cũng nên nhớ rằng quy ước gọi của hàm được chuyển làm đối số cho FunPtr phải khớp với quy ước gọi của hàm mà đối số được chuyển tới. Nói cách khác, hãy đi qua số &foo đến bar yêu cầu foobar để có cùng quy ước gọi điện.

  • tài kiểu dữ liệu

Xuất datatype người dùng thực sự là khá thẳng về phía trước. Đối với mọi kiểu dữ liệu cần được xuất, cá thể Storable phải được tạo cho loại này. Các trường hợp này chỉ định thông tin marshalling mà GHC cần để có thể xuất/nhập loại này. Trong số những thứ khác, bạn sẽ cần phải xác định các loại sizealignment của loại, cùng với cách đọc/ghi vào con trỏ các giá trị của loại đó. Tôi sử dụng một phần Hsc2hs cho tác vụ này (do đó các macro C trong tệp).

newtypes hoặc datatypes chỉ với một hàm tạo dễ dàng. Chúng trở thành một cấu trúc phẳng vì chỉ có một phương án có thể thay thế khi xây dựng/hủy các kiểu này. Các loại có nhiều hàm tạo trở thành một liên kết (một cấu trúc có thuộc tính Layout được đặt thành Explicit trong C#). Tuy nhiên chúng ta cũng cần phải bao gồm một enum để xác định cấu trúc nào đang được sử dụng.

nói chung, các kiểu dữ liệu Single định nghĩa là

data Single = Single { sint :: Int 
         , schar :: Char 
         } 

tạo sau Storable dụ

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 

và C struct

typedef struct Single Single_t; 

struct Single { 
    int sint; 
    wchar_t schar; 
} ; 

Chức năng foo :: Int -> Single sẽ được xuất khẩu như foo :: CInt -> Ptr Single Trong khi một datatype với nhiều nhà xây dựng

data Multi = Demi { mints :: [Int] 
        , mstring :: String 
        } 
      | Semi { semi :: [Single] 
        } 

tạo ra mã C sau:

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; 
} ; 

Các dụ Storable là tương đối thẳng về phía trước và cần làm theo dễ dàng hơn từ định nghĩa C struct.

  • loại Applied

phụ thuộc tracer của tôi sẽ cho phát ra cho cho các loại Maybe Int sự phụ thuộc vào cả hai loại IntMaybe. Điều này có nghĩa, rằng khi tạo ra các ví dụ Storable cho Maybe Int đầu trông giống như

instance Storable Int => Storable (Maybe Int) where 

Đó là, aslong như có một trường hợp storable cho các đối số của ứng dụng kiểu riêng của mình cũng có thể được xuất khẩu.

Maybe a được định nghĩa là có đối số đa hình Just a, khi tạo cấu trúc, một số thông tin loại bị mất. Các cấu trúc sẽ chứa đối số void*, mà bạn phải chuyển đổi theo cách thủ công thành loại phù hợp. Sự thay thế quá cồng kềnh trong quan điểm của tôi, đó là tạo ra các cấu trúc chuyên biệt. Ví dụ. struct MaybeInt. Nhưng số lượng cấu trúc đặc biệt có thể được tạo ra từ một mô-đun bình thường có thể nhanh chóng phát nổ theo cách này. (có thể thêm điều này làm cờ sau này).

Để dễ dàng mất thông tin này, công cụ của tôi sẽ xuất mọi tài liệu Haddock được tìm thấy cho hàm này dưới dạng nhận xét trong bao gồm được tạo. Nó cũng sẽ đặt chữ ký kiểu Haskell gốc trong bình luận. Một IDE sau đó sẽ trình bày chúng như là một phần của Intellisense (mã compeletion) của nó.

Như với tất cả các ví dụ này tôi đã sử dụng mã cho phía .NET của mọi thứ, Nếu bạn quan tâm đến việc bạn chỉ có thể xem kết quả của Hs2lib.

Có một vài loại khác cần điều trị đặc biệt. Cụ thể là ListsTuples.

  1. Danh sách cần phải vượt qua kích thước của mảng mà từ đó để sắp xếp lại, vì chúng ta đang giao tiếp với các ngôn ngữ không được quản lý mà kích thước của mảng không được biết rõ. Khi chúng ta trả về một danh sách, chúng ta cũng cần trả về kích thước của danh sách.
  2. Tuples được xây dựng đặc biệt theo loại, Để xuất chúng, trước tiên chúng tôi phải ánh xạ chúng thành kiểu dữ liệu "bình thường" và xuất chúng. Trong công cụ này được thực hiện cho đến 8-tuple.

    • loại đa hình

Vấn đề với các loại đa hình e.g. map :: (a -> b) -> [a] -> [b]size của ab không biết. Đó là, không có cách nào để dự trữ không gian cho các đối số và trả về giá trị vì chúng ta không biết chúng là gì. Tôi dự định hỗ trợ điều này bằng cách cho phép bạn chỉ định các giá trị có thể cho ab và tạo chức năng trình bao bọc chuyên biệt cho các loại này. Ở kích thước khác, trong ngôn ngữ bắt buộc, tôi sẽ sử dụng overloading để trình bày các loại bạn đã chọn cho người dùng.

Đối với các lớp học, giả định thế giới mở của Haskell thường là một vấn đề (ví dụ: một thể hiện có thể được thêm vào bất kỳ lúc nào). Tuy nhiên tại thời điểm biên dịch chỉ có một danh sách các trường hợp được biết đến tĩnh có sẵn. Tôi dự định cung cấp một tùy chọn sẽ tự động xuất nhiều trường hợp đặc biệt nhất có thể bằng cách sử dụng danh sách này. ví dụ. xuất khẩu (+) xuất chức năng chuyên biệt cho tất cả các trường hợp Num đã biết tại thời gian biên dịch (ví dụ: Int, Double, v.v.).

Công cụ này cũng khá đáng tin cậy. Vì tôi thực sự không thể kiểm tra mã cho độ tinh khiết, tôi luôn tin tưởng rằng lập trình viên là trung thực. Ví dụ. bạn không vượt qua một hàm có tác dụng phụ đối với một hàm mong đợi một hàm thuần túy. Hãy trung thực và đánh dấu đối số được đặt hàng cao hơn là không tinh khiết để tránh các vấn đề.

Tôi hy vọng điều này sẽ hữu ích và tôi hy vọng điều này không quá dài.

Cập nhật: Có phần nào đó của một bản ghi nhớ lớn mà tôi vừa mới phát hiện ra. Chúng ta phải nhớ rằng kiểu String trong .NET là không thay đổi. Vì vậy, khi marshaller gửi nó ra mã Haskell, CWString chúng ta nhận được ở đó là một bản sao của bản gốc. Chúng tôi để giải phóng việc này. Khi GC được thực hiện trong C# nó sẽ không ảnh hưởng đến CWString, mà là một bản sao.

Tuy nhiên, vấn đề là khi chúng tôi giải phóng nó trong mã Haskell, chúng tôi không thể sử dụng freeCWString. Con trỏ không được phân bổ với phân bổ C (msvcrt.dll). Có ba cách (mà tôi biết) để giải quyết vấn đề này.

  • sử dụng char * trong mã C# thay vì String khi gọi hàm Haskell. Sau đó bạn có con trỏ miễn phí khi bạn gọi trả về, hoặc khởi tạo hàm bằng cách sử dụng fixed.
  • nhập CoTaskMemFree trong Haskell và giải phóng con trỏ trong Haskell
  • sử dụng StringBuilder thay vì String. Tôi không hoàn toàn chắc chắn về điều này, nhưng ý tưởng là vì StringBuilder được thực hiện như là một con trỏ bản địa, Marshaller chỉ chuyển con trỏ này đến mã Haskell của bạn (mà cũng có thể cập nhật nó btw). Khi GC được thực hiện sau khi trả về cuộc gọi, StringBuilder sẽ được giải phóng.
+0

Tôi nghĩ rằng khá nhiều bao gồm tất cả mọi thứ. Tôi yêu các bạn/các bạn! :) –

+0

Bạn được chào đón nhiều nhất :) Nếu bạn có thêm bất kỳ câu hỏi nào, hãy hỏi :) – Phyx

4

Bạn đã thử xuất các chức năng qua FFI? Điều này cho phép bạn tạo thêm một giao diện C-ish cho các hàm. Tôi nghi ngờ rằng có thể gọi hàm Haskell trực tiếp từ C#. Xem tài liệu để biết thêm thông tin. (Liên kết ở trên).

Sau khi thực hiện một số thử nghiệm, tôi nghĩ rằng nói chung, không thể xuất các chức năng và chức năng bậc cao với loại thông số qua FFI. [Cần dẫn nguồn]

+0

Điều này chắc chắn có thể vì - như đã đề cập trong bài đăng - tôi đã thực hiện nó, nhưng chỉ với các chức năng đặt hàng đầu tiên. Và tất cả các hướng dẫn và ví dụ cũng được giới hạn trong các chức năng đặt hàng đầu tiên. –

+0

@lambdor Tôi hiểu lầm câu hỏi của bạn. Bài đăng trên blog mà bạn liên kết đã sử dụng cấu trúc xuất khẩu nước ngoài.Nếu bạn thấy liên kết mà tôi cung cấp, bạn sẽ thấy, GHC tạo ra một tệp '.c'-File chứa các nguyên mẫu cho các hàm được xuất. Bạn có thể thử và thực hiện một số thử nghiệm với các loại đơn đặt hàng cao. – fuz

+0

@lambdor Sau một số thử nghiệm, có vẻ như FFI chỉ cho phép các loại "đơn giản". Đó là: Không có biến kiểu, không có hàm bậc cao, không có ADT. – fuz

3

Được rồi, nhờ FUZxxl, giải pháp mà anh ấy đưa ra cho "loại không xác định". Lưu trữ dữ liệu trong Haskell MVar trong ngữ cảnh IO và liên lạc từ C# tới Haskell với các hàm bậc nhất. Đây có thể là giải pháp ít nhất cho các tình huống đơn giản.

Các vấn đề liên quan