wasm-platform-kit-build 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 brain-fuel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,2 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # Wasm Platform Kit build tools
2
+ Direct WIT-to-Haskell generation and application-owned component packaging. See the SDK repository documentation.
@@ -0,0 +1,369 @@
1
+ {-# LANGUAGE CPP #-}
2
+ #if !defined(wasm32_HOST_ARCH)
3
+ #error HostSDK.Canonical requires the wasm32 GHC backend
4
+ #endif
5
+ {-# LANGUAGE RecordWildCards #-}
6
+
7
+ -- | Memory32 canonical ABI support. Layouts come from upstream wit-parser.
8
+ -- Application modules never manipulate these pointers. ref:DEC-host-sdk-direct-haskell
9
+ module HostSDK.Canonical where
10
+
11
+ import Control.Exception (bracket, finally, onException)
12
+ import Control.Monad (forM, forM_, unless)
13
+ import Control.Monad.IO.Class (liftIO)
14
+ import Control.Monad.Trans.State.Strict (StateT, get, put, runStateT)
15
+ import qualified Data.ByteString as B
16
+ import Data.IORef
17
+ import Data.Int
18
+ import Data.Text (Text)
19
+ import qualified Data.Text.Encoding as T
20
+ import Data.Word
21
+ import Foreign.Marshal.Alloc (free, mallocBytes)
22
+ import Foreign.Marshal.Utils (copyBytes, fillBytes)
23
+ import Foreign.Ptr
24
+ import Foreign.StablePtr
25
+ import Foreign.Storable
26
+ import System.IO.Unsafe (unsafePerformIO)
27
+ import System.Mem (performMajorGC)
28
+
29
+ -- | Identifies whether lifting transfers responsibility for the input allocation.
30
+ data Ownership = Borrowed | Owned deriving (Eq)
31
+
32
+ -- | Tracks allocations for one call or one exported result, including nested values.
33
+ newtype Arena = Arena (IORef [Ptr Word8])
34
+
35
+ -- | Consumes the flattened integer lanes selected by upstream canonical layout.
36
+ type Lift = StateT [Word64] IO
37
+
38
+ -- | Pairs a Haskell type with its canonical memory and flattened representations.
39
+ data Codec a = Codec
40
+ { byteSize :: Int,
41
+ laneCount :: Int,
42
+ load :: Ownership -> Ptr Word8 -> IO a,
43
+ store :: Arena -> Ptr Word8 -> a -> IO (),
44
+ lower :: Arena -> a -> IO [Word64],
45
+ liftValue :: Ownership -> Lift a
46
+ }
47
+
48
+ -- | Starts an empty allocation scope before a call crosses the component boundary.
49
+ newArena :: IO Arena
50
+ newArena = Arena <$> newIORef []
51
+
52
+ -- | Frees every recorded allocation once, including on exceptional exits.
53
+ release :: Arena -> IO ()
54
+ release (Arena ref) = atomicModifyIORef' ref (\ps -> ([], ps)) >>= mapM_ free
55
+
56
+ -- | Brackets imported-call allocations so failed host operations also release them.
57
+ withArena :: (Arena -> IO a) -> IO a
58
+ withArena = bracket newArena release
59
+
60
+ -- | Records zeroed canonical storage in the current scope before exposing its address.
61
+ allocate :: Arena -> Int -> IO (Ptr Word8)
62
+ allocate (Arena ref) n = do
63
+ unless (n >= 0) (fail "canonical allocation overflow")
64
+ p <- mallocBytes (max 1 n)
65
+ modifyIORef' ref (p :)
66
+ fillBytes p 0 (max 1 n)
67
+ pure p
68
+
69
+ -- | Converts a pointer to its memory32 lane; this module rejects other architectures.
70
+ address :: Ptr a -> Word32
71
+ address = fromIntegral . ptrToWordPtr
72
+
73
+ -- | Reconstructs a memory32 address supplied by the canonical ABI.
74
+ pointer :: Word32 -> Ptr Word8
75
+ pointer = wordPtrToPtr . fromIntegral
76
+
77
+ -- | Consumes one lane and rejects an incomplete flattened argument.
78
+ word :: Lift Word32
79
+ word = do
80
+ xs <- get
81
+ case xs of
82
+ x : rest -> put rest >> pure (fromIntegral x)
83
+ _ -> liftIO (fail "canonical ABI: missing lane")
84
+
85
+ -- | Decodes a complete generated value from its flattened lane payload.
86
+ runLift :: Codec a -> Ownership -> [Word64] -> IO a
87
+ runLift codec own values = fst <$> runStateT (liftValue codec own) values
88
+
89
+ -- | Isolates a variant payload so its decoder cannot consume the next argument.
90
+ lanes :: Int -> Lift [Word64]
91
+ lanes n = do
92
+ xs <- get
93
+ unless (length xs >= n) (liftIO (fail "canonical ABI: missing payload"))
94
+ put (drop n xs)
95
+ pure (take n xs)
96
+
97
+ -- | Fills unused variant lanes to the width chosen by the canonical ABI.
98
+ pad :: Int -> [Word64] -> [Word64]
99
+ pad n xs = take n (xs ++ repeat 0)
100
+
101
+ -- | Releases transferred input storage while retaining borrowed memory.
102
+ freeOwned :: Ownership -> Ptr a -> IO ()
103
+ freeOwned own p = if own == Owned then free p else pure ()
104
+
105
+ -- | Represents an absent result payload without allocating any lanes.
106
+ unitCodec :: Codec ()
107
+ unitCodec = Codec 0 0 (\_ _ -> pure ()) (\_ _ _ -> pure ()) (\_ _ -> pure []) (\_ -> pure ())
108
+
109
+ -- | Uses the ABI little-endian memory32 representation for unsigned words.
110
+ u32Codec :: Codec Word32
111
+ u32Codec = integralCodec 4
112
+
113
+ -- | Keeps byte storage distinct from its widened i32 call representation.
114
+ u8Codec :: Codec Word8
115
+ u8Codec = integralCodec 1
116
+
117
+ -- | Keeps a complete i64 lane without converting through a machine-sized Int.
118
+ wideWord :: Lift Word64
119
+ wideWord = do
120
+ xs <- get
121
+ case xs of
122
+ x : rest -> put rest >> pure x
123
+ _ -> liftIO (fail "canonical ABI: missing lane")
124
+
125
+ -- | Memory width and flattening width are distinct for small integer types.
126
+ integralCodec :: (Integral a, Storable a) => Int -> Codec a
127
+ integralCodec size =
128
+ Codec
129
+ size
130
+ 1
131
+ (\_ p -> peek (castPtr p))
132
+ (\_ p x -> poke (castPtr p) x)
133
+ (\_ x -> pure [fromIntegral x])
134
+ (\_ -> fromIntegral <$> wideWord)
135
+
136
+ u16Codec :: Codec Word16
137
+ u16Codec = integralCodec 2
138
+
139
+ u64Codec :: Codec Word64
140
+ u64Codec = integralCodec 8
141
+
142
+ s8Codec :: Codec Int8
143
+ s8Codec = integralCodec 1
144
+
145
+ s16Codec :: Codec Int16
146
+ s16Codec = integralCodec 2
147
+
148
+ s32Codec :: Codec Int32
149
+ s32Codec = integralCodec 4
150
+
151
+ s64Codec :: Codec Int64
152
+ s64Codec = integralCodec 8
153
+
154
+ -- | Maps canonical boolean bytes and lanes to Haskell Bool.
155
+ boolCodec :: Codec Bool
156
+ boolCodec = Codec 1 1 (\_ p -> (/= 0) <$> peek p) (\_ p x -> poke p (if x then 1 else 0)) (\_ x -> pure [if x then 1 else 0]) (\_ -> (/= 0) <$> word)
157
+
158
+ -- | Copies UTF-8 into Haskell ownership and rejects invalid text on lifting.
159
+ textCodec :: Codec Text
160
+ textCodec = Codec 8 2 readText writeText lowerText liftText
161
+ where
162
+ readText own p = do
163
+ ptr <- peekByteOff p 0
164
+ len <- peekByteOff p 4
165
+ decode own ptr len
166
+ decode own ptr len = do
167
+ bytes <- B.packCStringLen (castPtr (pointer ptr), fromIntegral (len :: Word32))
168
+ freeOwned own (pointer ptr)
169
+ either (fail . show) pure (T.decodeUtf8' bytes)
170
+ lowerText arena value = do
171
+ let bytes = T.encodeUtf8 value
172
+ p <- allocate arena (B.length bytes)
173
+ B.useAsCStringLen bytes $ \(src, n) -> copyBytes p (castPtr src) n
174
+ pure [fromIntegral (address p), fromIntegral (B.length bytes)]
175
+ writeText arena p value = do
176
+ values <- lowerText arena value
177
+ case values of
178
+ [ptr, len] -> pokeByteOff p 0 (fromIntegral ptr :: Word32) >> pokeByteOff p 4 (fromIntegral len :: Word32)
179
+ _ -> fail "canonical text layout"
180
+ liftText own = do
181
+ ptr <- word
182
+ len <- word
183
+ liftIO (decode own ptr len)
184
+
185
+ -- | Uses upstream element stride and separately owns the array and nested values.
186
+ listCodec :: Codec a -> Codec [a]
187
+ listCodec item = Codec 8 2 readList writeList lowerList liftList
188
+ where
189
+ decode own ptr len = do
190
+ let p = pointer ptr
191
+ values <- forM [0 .. fromIntegral (len :: Word32) - 1] $ \i -> load item own (p `plusPtr` (i * byteSize item))
192
+ freeOwned own p
193
+ pure values
194
+ readList own p = do
195
+ ptr <- peekByteOff p 0
196
+ len <- peekByteOff p 4
197
+ decode own ptr len
198
+ lowerList arena values = do
199
+ let count = length values
200
+ unless (toInteger count * toInteger (byteSize item) <= 2147483647) (fail "canonical list allocation overflow")
201
+ p <- allocate arena (count * byteSize item)
202
+ forM_ (zip [0 ..] values) $ \(i, value) -> store item arena (p `plusPtr` (i * byteSize item)) value
203
+ pure [fromIntegral (address p), fromIntegral count]
204
+ writeList arena p values = do
205
+ raw <- lowerList arena values
206
+ case raw of
207
+ [ptr, len] -> pokeByteOff p 0 (fromIntegral ptr :: Word32) >> pokeByteOff p 4 (fromIntegral len :: Word32)
208
+ _ -> fail "canonical list layout"
209
+ liftList own = do
210
+ ptr <- word
211
+ len <- word
212
+ liftIO (decode own ptr len)
213
+
214
+ -- | Retains only the outstanding exported result until canonical post-return runs.
215
+ {-# NOINLINE returned #-}
216
+ returned :: IORef (Maybe Arena)
217
+ returned = unsafePerformIO (newIORef Nothing)
218
+
219
+ -- | Keeps exported buffers alive through lifting and detects a missing post-return.
220
+ exportResult :: Codec a -> IO a -> IO Word32
221
+ exportResult codec action = do
222
+ performMajorGC
223
+ value <- action
224
+ arena <- newArena
225
+ ( do
226
+ p <- allocate arena (byteSize codec)
227
+ store codec arena p value
228
+ old <- atomicModifyIORef' returned (\previous -> (Just arena, previous))
229
+ case old of
230
+ Nothing -> pure (address p)
231
+ Just previous -> release previous >> fail "canonical ABI: missing post-return"
232
+ )
233
+ `onException` release arena
234
+
235
+ -- | Releases the outstanding result after the host has copied its canonical value.
236
+ postReturn :: Word32 -> IO ()
237
+ postReturn _ = do
238
+ arena <- atomicModifyIORef' returned (\previous -> (Nothing, previous))
239
+ maybe (pure ()) release arena
240
+
241
+ -- | Direct results have one scalar lane and no aggregate return allocation.
242
+ exportScalar :: (Integral b) => Codec a -> IO a -> IO b
243
+ exportScalar codec action = withArena $ \arena -> do
244
+ value <- action
245
+ raw <- lower codec arena value
246
+ case raw of
247
+ [lane] -> pure (fromIntegral lane)
248
+ _ -> fail "canonical ABI: expected one result lane"
249
+
250
+ -- | An owned handle and a borrowed representation have distinct canonical meanings.
251
+ -- Constructors stay inside generated internals; applications use typed accessors.
252
+ data GuestResource a
253
+ = GuestOwned Word32 (IORef Bool)
254
+ | GuestBorrowed Word32 Integer
255
+ deriving (Eq)
256
+
257
+ instance Show (GuestResource a) where
258
+ show (GuestOwned _ _) = "<owned resource>"
259
+ show (GuestBorrowed _ _) = "<borrowed resource>"
260
+
261
+ -- | Tracks dynamic borrow scopes without claiming Haskell has linear lifetimes.
262
+ {-# NOINLINE guestCall #-}
263
+ guestCall :: IORef (Integer, Maybe Integer)
264
+ guestCall = unsafePerformIO (newIORef (0, Nothing))
265
+
266
+ withGuestCall :: IO a -> IO a
267
+ withGuestCall action = bracket enter leave (const action)
268
+ where
269
+ enter = atomicModifyIORef' guestCall $ \(next, active) ->
270
+ ((next + 1, Just next), active)
271
+ leave previous = modifyIORef' guestCall (\(next, _) -> (next, previous))
272
+
273
+ ownedGuestResource :: Word32 -> IO (GuestResource a)
274
+ ownedGuestResource handle = GuestOwned handle <$> newIORef True
275
+
276
+ borrowedGuestResource :: Word32 -> IO (GuestResource a)
277
+ borrowedGuestResource rep = do
278
+ (_, active) <- readIORef guestCall
279
+ maybe (fail "canonical ABI: missing borrow scope") (pure . GuestBorrowed rep) active
280
+
281
+ -- | Retains typed application state until the component resource destructor runs.
282
+ newGuestResource :: (Word32 -> IO Word32) -> a -> IO (GuestResource a)
283
+ newGuestResource create value = do
284
+ stable <- newStablePtr value
285
+ (create (address (castStablePtrToPtr stable)) >>= ownedGuestResource)
286
+ `onException` freeStablePtr stable
287
+
288
+ -- | Borrowed representations are usable only during the call that created them.
289
+ readGuestResource :: (Word32 -> IO Word32) -> GuestResource a -> IO a
290
+ readGuestResource rep resource = do
291
+ raw <- case resource of
292
+ GuestOwned _ _ -> guestHandle resource >>= rep
293
+ GuestBorrowed value epoch -> do
294
+ (_, active) <- readIORef guestCall
295
+ unless (active == Just epoch) (fail "canonical ABI: expired resource borrow")
296
+ pure value
297
+ deRefStablePtr (castPtrToStablePtr (castPtr (pointer raw)))
298
+
299
+ -- | Runs application cleanup exactly at destruction, including cleanup exceptions.
300
+ destroyGuestResource :: (a -> IO ()) -> Word32 -> IO ()
301
+ destroyGuestResource destroy raw = do
302
+ let stable = castPtrToStablePtr (castPtr (pointer raw))
303
+ (deRefStablePtr stable >>= destroy) `finally` freeStablePtr stable
304
+
305
+ -- | Borrowed handles cannot be dropped or transferred as owned values.
306
+ guestHandle :: GuestResource a -> IO Word32
307
+ guestHandle (GuestOwned handle live) = do
308
+ valid <- readIORef live
309
+ unless valid (fail "canonical ABI: resource ownership already transferred")
310
+ pure handle
311
+ guestHandle (GuestBorrowed _ _) = fail "canonical ABI: borrowed resource cannot escape its call"
312
+
313
+ -- | Invalidates aliases before an owned handle crosses the boundary or is dropped.
314
+ takeGuestHandle :: GuestResource a -> IO Word32
315
+ takeGuestHandle resource@(GuestOwned _ live) = do
316
+ handle <- guestHandle resource
317
+ writeIORef live False
318
+ pure handle
319
+ takeGuestHandle (GuestBorrowed _ _) = fail "canonical ABI: borrowed resource cannot transfer ownership"
320
+
321
+ guestResourceCodec :: Bool -> Codec (GuestResource a)
322
+ guestResourceCodec borrowed =
323
+ Codec
324
+ 4
325
+ 1
326
+ (\own p -> load u32Codec own p >>= construct)
327
+ (\arena p value -> lowerHandle value >>= store u32Codec arena p)
328
+ (\arena value -> lowerHandle value >>= lower u32Codec arena)
329
+ (\_ -> word >>= liftIO . construct)
330
+ where
331
+ construct = if borrowed then borrowedGuestResource else ownedGuestResource
332
+ lowerHandle = if borrowed then guestHandle else takeGuestHandle
333
+
334
+ -- | Wraps an opaque resource without exposing its representation to application code.
335
+ mapCodec :: (a -> b) -> (b -> a) -> Codec a -> Codec b
336
+ mapCodec wrap unwrap codec =
337
+ Codec
338
+ (byteSize codec)
339
+ (laneCount codec)
340
+ (\own p -> wrap <$> load codec own p)
341
+ (\arena p value -> store codec arena p (unwrap value))
342
+ (\arena value -> lower codec arena (unwrap value))
343
+ (\own -> wrap <$> liftValue codec own)
344
+
345
+ -- | Bulk binary values use ByteString rather than one heap object per octet.
346
+ bytesCodec :: Codec B.ByteString
347
+ bytesCodec = Codec 8 2 readBytes writeBytes lowerBytes liftBytes
348
+ where
349
+ decode own ptr len = do
350
+ value <- B.packCStringLen (castPtr (pointer ptr), fromIntegral (len :: Word32))
351
+ freeOwned own (pointer ptr)
352
+ pure value
353
+ readBytes own p = do
354
+ ptr <- peekByteOff p 0
355
+ len <- peekByteOff p 4
356
+ decode own ptr len
357
+ lowerBytes arena value = do
358
+ p <- allocate arena (B.length value)
359
+ B.useAsCStringLen value $ \(source, count) -> copyBytes p (castPtr source) count
360
+ pure [fromIntegral (address p), fromIntegral (B.length value)]
361
+ writeBytes arena p value = do
362
+ raw <- lowerBytes arena value
363
+ case raw of
364
+ [ptr, len] -> pokeByteOff p 0 (fromIntegral ptr :: Word32) >> pokeByteOff p 4 (fromIntegral len :: Word32)
365
+ _ -> fail "canonical byte buffer layout"
366
+ liftBytes own = do
367
+ ptr <- word
368
+ len <- word
369
+ liftIO (decode own ptr len)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 brain-fuel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ cabal-version: 3.0
2
+ name: host-sdk-guest
3
+ version: 0.2.0
4
+ license: MIT
5
+ license-file: LICENSE
6
+ build-type: Simple
7
+ synopsis: Canonical memory32 support for generated typed Haskell components
8
+ library
9
+ exposed-modules: HostSDK.Canonical
10
+ hs-source-dirs: .
11
+ build-depends: base >= 4.18 && < 5, bytestring, text, transformers
12
+ default-language: Haskell2010
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "wasm-platform-kit-build",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "exports": {
7
+ ".": "./tools/index.mjs",
8
+ "./package.json": "./package.json"
9
+ },
10
+ "files": [
11
+ "tools",
12
+ "haskell",
13
+ "wit",
14
+ "LICENSE",
15
+ "README.md"
16
+ ],
17
+ "dependencies": {
18
+ "wasm-platform-kit": "0.2.0",
19
+ "@bytecodealliance/jco": "1.41.0",
20
+ "prettier": "3.9.10",
21
+ "prettier-plugin-brace-style": "0.10.4"
22
+ },
23
+ "optionalDependencies": {
24
+ "wasm-platform-kit-darwin-arm64": "0.2.0",
25
+ "wasm-platform-kit-darwin-x64": "0.2.0",
26
+ "wasm-platform-kit-linux-arm64": "0.2.0",
27
+ "wasm-platform-kit-linux-x64": "0.2.0"
28
+ }
6
29
  }