1
1
mirror of https://github.com/tweag/asterius.git synced 2024-09-21 05:48:04 +03:00

"foreign export javascript" is working, included in jsffi unit test. (+3 squashed commit)

Squashed commit:

[a55fa61] no message

[4aa1467] no message

[961b519] no message
This commit is contained in:
Shao Cheng 2018-08-19 12:47:22 +08:00
parent be04b9a72d
commit 4d02e1fd96
9 changed files with 127 additions and 49 deletions

View File

@ -25,9 +25,11 @@ What works currently:
* All GHC language features except Template Haskell. * All GHC language features except Template Haskell.
* Non-IO parts in `ghc-prim`/`integer-simple`/`base`/`array`/`deepseq`/`containers`/`transformers`/`mtl`/`pretty`. IO is achieved via rts primitives like `print_i64` or JavaScript FFI. * Non-IO parts in `ghc-prim`/`integer-simple`/`base`/`array`/`deepseq`/`containers`/`transformers`/`mtl`/`pretty`. IO is achieved via rts primitives like `print_i64` or JavaScript FFI.
* Importing JavaScript expressions via the `foreign import javascript` syntax. First-class `JSRef` type in Haskell land. * Importing JavaScript expressions via the `foreign import javascript` syntax. First-class `JSRef` type in Haskell land.
* Calling Haskell functions from JavaScript via the `foreign export javascript` syntax. Haskell closures can be passed between Haskell/JavaScript boundary via `StablePtr`.
* Invoking RTS API on the JavaScript side to manipulate Haskell closures and trigger evaluation. * Invoking RTS API on the JavaScript side to manipulate Haskell closures and trigger evaluation.
* A linker which performs aggressive dead-code elimination, producing as small WebAssembly binary as possible.
* A debugger which outputs memory loads/stores and control flow transfers. * A debugger which outputs memory loads/stores and control flow transfers.
* A monadic EDSL to construct WebAssembly code directly in Haskell. * Complete `binaryen` raw bindings, plus a monadic EDSL to construct WebAssembly code directly in Haskell.
* Besides WebAssembly MVP & the experimental BigInt support, no special requirements on the underlying JavaScript engine. * Besides WebAssembly MVP & the experimental BigInt support, no special requirements on the underlying JavaScript engine.
Better check the [`fib`](asterius/test/fib/fib.hs), [`jsffi`](asterius/test/jsffi/jsffi.hs), [`array`](asterius/test/array/array.hs) and [`rtsapi`](asterius/test/rtsapi.hs) test suites first to get some idea on current capabilities of `asterius`. Better check the [`fib`](asterius/test/fib/fib.hs), [`jsffi`](asterius/test/jsffi/jsffi.hs), [`array`](asterius/test/array/array.hs) and [`rtsapi`](asterius/test/rtsapi.hs) test suites first to get some idea on current capabilities of `asterius`.

View File

@ -46,12 +46,12 @@ data Task = Task
, heapSize :: Int , heapSize :: Int
, asteriusInstanceCallback :: String , asteriusInstanceCallback :: String
, extraGHCFlags :: [String] , extraGHCFlags :: [String]
, extraRootSymbols :: [AsteriusEntitySymbol] , exportFunctions, extraRootSymbols :: [AsteriusEntitySymbol]
} }
parseTask :: Parser Task parseTask :: Parser Task
parseTask = parseTask =
(\i m_wasm m_node m_report m_gv dbg opt ir r m_hs m_with_i ghc_flags root_syms -> (\i m_wasm m_node m_report m_gv dbg opt ir r m_hs m_with_i ghc_flags export_funcs root_syms ->
Task Task
{ input = i { input = i
, outputWasm = fromMaybe (i -<.> "wasm") m_wasm , outputWasm = fromMaybe (i -<.> "wasm") m_wasm
@ -68,6 +68,8 @@ parseTask =
"i => {\ni.wasmInstance.exports.hs_init();\ni.wasmInstance.exports.rts_evalLazyIO(i.staticsSymbolMap.MainCapability, i.staticsSymbolMap.Main_main_closure, 0);\n}" "i => {\ni.wasmInstance.exports.hs_init();\ni.wasmInstance.exports.rts_evalLazyIO(i.staticsSymbolMap.MainCapability, i.staticsSymbolMap.Main_main_closure, 0);\n}"
m_with_i m_with_i
, extraGHCFlags = ghc_flags , extraGHCFlags = ghc_flags
, exportFunctions =
[AsteriusEntitySymbol {entityName = sym} | sym <- export_funcs]
, extraRootSymbols = , extraRootSymbols =
[AsteriusEntitySymbol {entityName = sym} | sym <- root_syms] [AsteriusEntitySymbol {entityName = sym} | sym <- root_syms]
}) <$> }) <$>
@ -103,6 +105,8 @@ parseTask =
help help
"Supply a JavaScript callback expression which will be invoked on the initiated asterius instance. Defaults to calling Main.main")) <*> "Supply a JavaScript callback expression which will be invoked on the initiated asterius instance. Defaults to calling Main.main")) <*>
many (strOption (long "ghc-option" <> help "Extra GHC flags")) <*> many (strOption (long "ghc-option" <> help "Extra GHC flags")) <*>
many
(strOption (long "export-function" <> help "Symbol of exported function")) <*>
many many
(strOption (strOption
(long "extra-root-symbol" <> (long "extra-root-symbol" <>
@ -231,12 +235,15 @@ main = do
final_store <- readIORef final_store_ref final_store <- readIORef final_store_ref
putStrLn "[INFO] Attempting to link into a standalone WebAssembly module" putStrLn "[INFO] Attempting to link into a standalone WebAssembly module"
let (!m_final_m, !report) = let (!m_final_m, !report) =
linkStart debug final_store $ linkStart
HS.fromList $ debug
extraRootSymbols <> final_store
[ AsteriusEntitySymbol {entityName = internalName} (HS.fromList $
| FunctionExport {..} <- V.toList $ rtsAsteriusFunctionExports debug extraRootSymbols <>
] [ AsteriusEntitySymbol {entityName = internalName}
| FunctionExport {..} <- V.toList $ rtsAsteriusFunctionExports debug
])
exportFunctions
maybe maybe
(pure ()) (pure ())
(\p -> do (\p -> do

View File

@ -24,6 +24,7 @@ module Asterius.Builtins
, wasmPageSize , wasmPageSize
, cutI64 , cutI64
, generateWasmFunctionTypeName , generateWasmFunctionTypeName
, generateWrapperFunction
) where ) where
import Asterius.BuildInfo import Asterius.BuildInfo

View File

@ -14,6 +14,7 @@ module Asterius.JSFFI
, generateFFIImportObjectFactory , generateFFIImportObjectFactory
) where ) where
import Asterius.Builtins
import Asterius.Internals import Asterius.Internals
import Asterius.Types import Asterius.Types
import Asterius.TypesConv import Asterius.TypesConv
@ -509,7 +510,21 @@ generateFFIExportFunction FFIExportDecl {..} =
AsteriusEntitySymbol AsteriusEntitySymbol
{entityName = "rts_get" <> hsTyCon ffi_result_t} {entityName = "rts_get" <> hsTyCon ffi_result_t}
, operands = , operands =
[UnresolvedGetLocal {unresolvedLocalReg = ret}] [ Load
{ signed = False
, bytes = 8
, offset = 0
, align = 0
, valueType = I64
, ptr =
Unary
{ unaryOp = WrapInt64
, operand0 =
UnresolvedGetLocal
{unresolvedLocalReg = ret}
}
}
]
, valueType = , valueType =
recoverWasmWrapperValueType $ Just ffi_result_t recoverWasmWrapperValueType $ Just ffi_result_t
} }
@ -531,7 +546,8 @@ generateFFIWrapperModule mod_ffi_state@FFIMarshalState {..} =
[ (fromString $ recoverWasmWrapperFunctionName mk k, wrapper_func) [ (fromString $ recoverWasmWrapperFunctionName mk k, wrapper_func)
| (mk, k, wrapper_func) <- import_wrapper_funcs | (mk, k, wrapper_func) <- import_wrapper_funcs
] <> ] <>
export_funcs export_funcs <>
export_wrapper_funcs
, ffiMarshalState = mod_ffi_state , ffiMarshalState = mod_ffi_state
} }
where where
@ -545,6 +561,12 @@ generateFFIWrapperModule mod_ffi_state@FFIMarshalState {..} =
| mod_ffi_decls <- HM.elems ffiExportDecls | mod_ffi_decls <- HM.elems ffiExportDecls
, (k, ffi_decl) <- HM.toList mod_ffi_decls , (k, ffi_decl) <- HM.toList mod_ffi_decls
] ]
export_wrapper_funcs =
[ ( AsteriusEntitySymbol
{entityName = "__asterius_jsffi_export_" <> entityName k}
, generateWrapperFunction k f)
| (k, f) <- export_funcs
]
generateFFIFunctionImports :: FFIMarshalState -> [AsteriusFunctionImport] generateFFIFunctionImports :: FFIMarshalState -> [AsteriusFunctionImport]
generateFFIFunctionImports FFIMarshalState {..} = generateFFIFunctionImports FFIMarshalState {..} =

View File

@ -226,14 +226,16 @@ mergeSymbols ::
Bool Bool
-> AsteriusStore -> AsteriusStore
-> HS.HashSet AsteriusEntitySymbol -> HS.HashSet AsteriusEntitySymbol
-> [AsteriusEntitySymbol]
-> (Maybe AsteriusModule, LinkReport) -> (Maybe AsteriusModule, LinkReport)
mergeSymbols debug AsteriusStore {..} syms = (maybe_final_m, final_rep) mergeSymbols debug AsteriusStore {..} root_syms export_funcs =
(maybe_final_m, final_rep)
where where
maybe_final_m maybe_final_m
| HS.null (unfoundSymbols final_rep) && | HS.null (unfoundSymbols final_rep) &&
HS.null (unavailableSymbols final_rep) = Just final_m HS.null (unavailableSymbols final_rep) = Just final_m
| otherwise = Nothing | otherwise = Nothing
(_, final_rep, final_m) = go (syms, mempty, mempty) (_, final_rep, final_m) = go (root_syms, mempty, mempty)
go i@(i_staging_syms, _, _) go i@(i_staging_syms, _, _)
| HS.null i_staging_syms = o | HS.null i_staging_syms = o
| otherwise = go o | otherwise = go o
@ -266,7 +268,7 @@ mergeSymbols debug AsteriusStore {..} syms = (maybe_final_m, final_rep)
{ functionMap = { functionMap =
[ ( i_staging_sym [ ( i_staging_sym
, patchWritePtrArrayOp $ , patchWritePtrArrayOp $
maskUnknownCCallTargets $ maskUnknownCCallTargets export_funcs $
resolveGlobalRegs func) resolveGlobalRegs func)
] ]
} }
@ -423,11 +425,12 @@ resolveFunctionImport AsteriusFunctionImport {..} =
resolveAsteriusModule :: resolveAsteriusModule ::
Bool Bool
-> FFIMarshalState -> FFIMarshalState
-> [AsteriusEntitySymbol]
-> AsteriusModule -> AsteriusModule
-> ( Module -> ( Module
, HM.HashMap AsteriusEntitySymbol Int64 , HM.HashMap AsteriusEntitySymbol Int64
, HM.HashMap AsteriusEntitySymbol Int64) , HM.HashMap AsteriusEntitySymbol Int64)
resolveAsteriusModule debug bundled_ffi_state m_globals_resolved = resolveAsteriusModule debug bundled_ffi_state export_funcs m_globals_resolved =
( Module ( Module
{ functionTypeMap = { functionTypeMap =
HM.fromList HM.fromList
@ -462,7 +465,13 @@ resolveAsteriusModule debug bundled_ffi_state m_globals_resolved =
V.fromList [resolveFunctionImport imp | imp <- func_imports] V.fromList [resolveFunctionImport imp | imp <- func_imports]
, tableImports = [] , tableImports = []
, globalImports = [] , globalImports = []
, functionExports = rtsAsteriusFunctionExports debug , functionExports =
rtsAsteriusFunctionExports debug <>
V.fromList
[ FunctionExport
{internalName = "__asterius_jsffi_export_" <> k, externalName = k}
| k <- map entityName export_funcs
]
, tableExports = [] , tableExports = []
, globalExports = [] , globalExports = []
, globalMap = [] , globalMap = []
@ -486,12 +495,23 @@ linkStart ::
Bool Bool
-> AsteriusStore -> AsteriusStore
-> HS.HashSet AsteriusEntitySymbol -> HS.HashSet AsteriusEntitySymbol
-> [AsteriusEntitySymbol]
-> (Maybe Module, LinkReport) -> (Maybe Module, LinkReport)
linkStart debug store syms = linkStart debug store root_syms export_funcs =
( maybe_result_m ( maybe_result_m
, report {staticsSymbolMap = ss_sym_map, functionSymbolMap = func_sym_map}) , report {staticsSymbolMap = ss_sym_map, functionSymbolMap = func_sym_map})
where where
(maybe_merged_m, report) = mergeSymbols debug store syms (maybe_merged_m, report) =
mergeSymbols
debug
store
(root_syms <>
HS.fromList
[ AsteriusEntitySymbol
{entityName = "__asterius_jsffi_export_" <> entityName k}
| k <- export_funcs
])
export_funcs
(maybe_result_m, ss_sym_map, func_sym_map) = (maybe_result_m, ss_sym_map, func_sym_map) =
case maybe_merged_m of case maybe_merged_m of
Just merged_m -> (Just result_m, ss_sym_map', func_sym_map') Just merged_m -> (Just result_m, ss_sym_map', func_sym_map')
@ -499,6 +519,7 @@ linkStart debug store syms =
resolveAsteriusModule resolveAsteriusModule
debug debug
(bundledFFIMarshalState report) (bundledFFIMarshalState report)
export_funcs
merged_m merged_m
_ -> (Nothing, mempty, mempty) _ -> (Nothing, mempty, mempty)

View File

@ -32,35 +32,41 @@ patchWritePtrArrayOp t =
where where
go = gmapT patchWritePtrArrayOp t go = gmapT patchWritePtrArrayOp t
maskUnknownCCallTargets :: Data a => a -> a maskUnknownCCallTargets :: Data a => [AsteriusEntitySymbol] -> a -> a
maskUnknownCCallTargets t = maskUnknownCCallTargets export_funcs = f
case eqTypeRep (typeOf t) (typeRep :: TypeRep Expression) of
Just HRefl ->
case t of
Call {..}
| not $ has_call_target target ->
marshalErrorCode errUnimplemented valueType
| target == "createIOThread" ->
case V.toList operands of
[cap, stack_size_w@Load {valueType = I32}, target_closure] ->
t
{ operands =
[ cap
, Unary {unaryOp = ExtendUInt32, operand0 = stack_size_w}
, target_closure
]
}
_ -> t
| otherwise -> t
AtomicRMW {} -> marshalErrorCode errAtomics I64
AtomicLoad {} -> marshalErrorCode errAtomics I64
AtomicStore {} -> marshalErrorCode errAtomics None
AtomicCmpxchg {} -> marshalErrorCode errAtomics I64
_ -> go
_ -> go
where where
go = gmapT maskUnknownCCallTargets t
has_call_target sym = has_call_target sym =
"__asterius" `BS.isPrefixOf` SBS.fromShort (entityName sym) || "__asterius" `BS.isPrefixOf` SBS.fromShort (entityName sym) ||
sym `HM.member` sym `HM.member`
functionMap (rtsAsteriusModule unsafeDefaultBuiltinsOptions) functionMap (rtsAsteriusModule unsafeDefaultBuiltinsOptions) ||
sym `elem` export_funcs
f :: Data a => a -> a
f t =
let go = gmapT f t
in case eqTypeRep (typeOf t) (typeRep :: TypeRep Expression) of
Just HRefl ->
case t of
Call {..}
| not $ has_call_target target ->
marshalErrorCode errUnimplemented valueType
| target == "createIOThread" ->
case V.toList operands of
[cap, stack_size_w@Load {valueType = I32}, target_closure] ->
t
{ operands =
[ cap
, Unary
{ unaryOp = ExtendUInt32
, operand0 = stack_size_w
}
, target_closure
]
}
_ -> t
| otherwise -> t
AtomicRMW {} -> marshalErrorCode errAtomics I64
AtomicLoad {} -> marshalErrorCode errAtomics I64
AtomicStore {} -> marshalErrorCode errAtomics None
AtomicCmpxchg {} -> marshalErrorCode errAtomics I64
_ -> go
_ -> go

View File

@ -9,7 +9,16 @@ main = do
, "test/jsffi/jsffi.hs" , "test/jsffi/jsffi.hs"
, "--output-link-report" , "--output-link-report"
, "test/jsffi/jsffi.link.txt" , "test/jsffi/jsffi.link.txt"
, "--extra-root-symbol=mult_hs" , "--export-function=mult_hs"
, "--run" , "--run"
] <> ] <>
[ mconcat
[ "--asterius-instance-callback="
, "i => {"
, "i.wasmInstance.exports.hs_init();"
, "i.wasmInstance.exports.main();"
, "console.log(i.wasmInstance.exports.mult_hs(6, 7));"
, "}"
]
] <>
args args

View File

@ -15,7 +15,7 @@ Requirements:
* Implement `StablePtr`, so that Haskell closures can be safely passed between Haskell/JavaScript boundary without being garbage collected. (done, see `stableptr` unit test) * Implement `StablePtr`, so that Haskell closures can be safely passed between Haskell/JavaScript boundary without being garbage collected. (done, see `stableptr` unit test)
* Implement `RtsAPI`, so that JavaScript code can create Haskell closures, trigger evaluation and inspect results. (done, see `rtsapi` unit test) * Implement `RtsAPI`, so that JavaScript code can create Haskell closures, trigger evaluation and inspect results. (done, see `rtsapi` unit test)
* Add `StablePtr` to JSFFI basic types. (done, see `jsffi` unit test) * Add `StablePtr` to JSFFI basic types. (done, see `jsffi` unit test)
* Implement `foreign export javascript` syntax, add necessary logic in `JSFFI`/`Resolve` (in progress) * Implement `foreign export javascript` syntax, add necessary logic in `JSFFI`/`Resolve` (done, see `jsffi` unit test)
Rough ETA: before Aug 20th. Rough ETA: before Aug 20th.

View File

@ -8,7 +8,7 @@ foreign import javascript "new Date()" current_time :: IO JSRef
foreign import javascript "console.log(${1})" js_print :: JSRef -> IO () foreign import javascript "console.log(${1})" js_print :: JSRef -> IO ()
``` ```
The source text of `foreign import javascript` should be a valid JavaScript expression (but you can use something like `${1}`, `${2}` to refer to the function parameters). Supported types are: The source text of `foreign import javascript` should be a valid JavaScript expression (but you can use something like `${1}`, `${2}` to refer to the function parameters). Supported basic types are:
* `Ptr` * `Ptr`
* `FunPtr` * `FunPtr`
@ -20,4 +20,14 @@ The source text of `foreign import javascript` should be a valid JavaScript expr
* `Double` * `Double`
* `JSRef` * `JSRef`
The result can be wrapped in `IO` (or not).
`JSRef` is a magic type that doesn't actually appear in any module's source code. In the Haskell land, `JSRef` is first-class and opaque: you can pass it around, put it in a data structure, etc, but under the hood it's just a handle. The runtime maintains mappings from handles to real JavaScript objects. `JSRef` is a magic type that doesn't actually appear in any module's source code. In the Haskell land, `JSRef` is first-class and opaque: you can pass it around, put it in a data structure, etc, but under the hood it's just a handle. The runtime maintains mappings from handles to real JavaScript objects.
Also, a prototype of `foreign export javascript` is implemented, check `jsffi` for details. The syntax is roughly:
```
foreign export javascript "mult_hs" (*) :: Int -> Int -> Int
```
In a Haskell module, one can specify the exported function name (must be globally unique), along with its Haskell identifier and type. One can specify `ahc-link --export-function=mult_hs` to make the linker include the relevant bits in final WebAssembly binary, and export `mult_hs` as a regular WebAssembly export function. After calling `hs_init` to initialize the runtime, one can call `mult_hs` just like a regular JavaScript function.