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.
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
và "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 foo
và bar
để có cùng quy ước gọi điện.
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 size
và alignment
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.
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 Int
và Maybe
. Đ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.
Vì 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à Lists
và Tuples
.
- 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.
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.
Vấn đề với các loại đa hình e.g. map :: (a -> b) -> [a] -> [b]
là size
của a
và b
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 a
và b
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 có để 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.
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! :) –
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