SoOSiM - Abstract Full System Simulator
Installation
Creating OS Components
We jump straight into some code, by showing the description of the Memory Manager (http://www.soos-project.eu/wiki/index.php/Application_Cases#Memory_Manager)
./examples/MemoryManager.hs
{-# LANGUAGE TypeFamilies #-}
module MemoryManager where
import SoOSiM
import MemoryManager.Types
import MemoryManager.Util
memoryManager :: MemState -> Input MemCommand -> Sim MemState
memoryManager s (Message content retAddr) = do
case content of
(Register memorySource) -> do
yield $ s {addressLookup = memorySource:(addressLookup s)}
(Read addr) -> do
let src = checkAddress (addressLookup s) addr
case (sourceId src) of
Nothing -> do
addrVal <- readMemory addr
respond MemoryManager retAddr addrVal
yield s
Just remote -> do
response <- invoke MemoryManager remote content
respond MemoryManager retAddr response
yield s
(Write addr val) -> do
let src = checkAddress (addressLookup s) addr
case (sourceId src) of
Nothing -> do
addrVal <- writeMemory addr val
yield s
Just remote -> do
invokeAsync MemoryManager remote content ignore
yield s
memoryManager s _ = yield s
instance ComponentInterface MemoryManager where
type State MemoryManager = MemState
type Receive MemoryManager = MemCommand
type Send MemoryManager = Dynamic
initState _ = (MemState [])
componentName _ = "MemoryManager"
componentBehaviour _ = memoryManager
./examples/MemoryManager/Types.hs
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ExistentialQuantification #-}
module MemoryManager.Types where
import SoOSiM
data MemorySource
= MemorySource
{ baseAddress :: Int
, scope :: Int
, sourceId :: Maybe ComponentId
}
data MemState =
MemState { addressLookup :: [MemorySource]
}
data MemCommand = Register Int Int (Maybe ComponentId)
| Read Int
| forall a . Typeable a => Write Int a
deriving Typeable
data MemoryManager = MemoryManager
./examples/MemoryManager/Util.hs
module MemoryManager.Util where
import MemoryManager.Types
checkAddress ::
[MemorySource]
-> Int
-> MemorySource
checkAddress sources addr = case (filter containsAddr sources) of
[] -> error ("address unknown: " ++ show addr)
(x:_) -> x
where
containsAddr (MemorySource base sc _) = base <= addr && addr < sc
Component definition Step-by-Step
We will now walk through the code step-by-step:
module MemoryManager where
We start by defining the name of our Haskell module, in this case MemoryManager
.
Make sure the name of file matches the name of the module, where haskell src files use the .hs
file-name extension.
We continue with importing modules that we require to build our component:
import SoOSiM
import MemoryManager.Types
import MemoryManager.Util
The SoOSiM
module defines all the simulator API functions.
Besides the external module, we also import two local module called MemoryManager.Types
and MemoryManager.Util
, which we define in ./MemoryManager/Types.hs
and ./MemoryManager/Util.hs
respectively.
We start our description with a datatype definition describing the internal state of our memory manager component, and the datatype encoding the messages our memory manager will receive:
data MemorySource
= MemorySource
{ baseAddress :: Int
, scope :: Int
, sourceId :: Maybe ComponentId
}
data MemState =
MemState { addressLookup :: [MemorySource]
}
data MemCommand = Register MemorySource
| Read Int
| forall a . Typeable a => Write Int a
deriving Typeable
data MemoryManager = MemoryManager
We define two record datatypes [1]; and with three fields (baseAddress
, scope
, and sourceId
) and another with one field (addressLookup
).
The first record type defines an address range (baseAddress
and scope
) and an indication which memory manager is responsible for tht memory range.
The second record type, which has only one field, which defines a dynamically-sized list of MemorySource
elements.
The third datatype is an algebraic datatype defining the kind of messages that can be send to the memory manager: registering a memory range, reading, and writing.
The fourth datatype is a singleton datatype, which will act as the label/name for the interface defining our memory manager.
We now start defining the actual behaviour of our memory manager, starting with its type annotation:
memoryManager :: MemState -> Input MemCommand -> Sim MemState
The type definition tells us that the first argument has the type of our internal component state, and the second argument a value of type Input a
, where the a
is instantiate to the MemCommand
datatype.
The possible values of the Input a
type are enumerated in the OS Component API section.
The value of the result is of type Sim MemState
.
This tells us two things:
- The
memoryManager
function is executed within the Sim
monad.
- The actual value that is returned is of type
MemState
.
A monad is many wonderful things [2], way too much to explain here, so for the rest of this README we see it as an execution environment.
Only inside this execution environment will we have access to the SoOSiM API functions.
Although we know the types of the arguments and the result of the function, we don't know their actual meaning.
The SoOSiM simulator will call your component behaviour, passing as the first argument its current internal state.
The second argument is an event that triggered the execution of your component: for example a message send to you by another component.
The result that you must ultimately return is the, potentially updated, internal state of your component.
We now turn to the first line of the actual function definition:
memoryManager s (Message content retAddr) = do
Where memoryManager
is the name of the function, s
the first argument (of type MemState
).
We pattern-match on the second argument, meaning this function definition clause only works for values whose constructor is Message
.
By pattern matching we get access to the fields of the datatype, where we bind the names content
and retAddr
to the values of these fields.
The do
keyword after the =
sign indicates that the function executes within a monadic environment, the Sim
environment in our case.
The semantics in a monadic environment are different from those in a normal Haskell functions.
A monadic environment has a more imperative feel, in which your function definition interacts with the environment step-by-step, statement after statements.
This also gives rise to the scoping rules familiar to the imperative programmer: names cannot be used before they are declared.
Next we define a nested case
-statement that contains most of the actual behaviour of our memory manager component:
case content of
(Register memorySource) -> do
yield $ s {addressLookup = memorySource:(addressLookup s)}
(Read addr) -> do
let src = checkAddress (addressLookup s) addr
case (sourceId src) of
Nothing -> do
addrVal <- readMemory addr
respond MemoryManager retAddr addrVal
yield s
Just remote -> do
response <- invoke MemoryManager remote content
respond MemoryManager retAddr response
yield s
(Write addr val) -> do
let src = checkAddress (addressLookup s) addr
case (sourceId src) of
Nothing -> do
addrVal <- writeMemory addr val
yield s
Just remote -> do
invokeAsync MemoryManager remote content ignore
yield s
In the first alternative of our case-statement we handle a Register
message, by updating our address lookup table with an additional memory source.
We yield
to the simulator with our updated internal state.
In the second alternative we handle a Read
request.
The next line in our function definition, which checks which specific memory manager is responsible for the address, is:
let src = checkAddress (addressLookup s) addr
Haskell is white-space sensitive, so make sure that you have a good editor that does automatic indenting.
We use the let
construct to bind the expression checkAddress (addressLookup s) addr
to the name src
.
We use these let-bindings to bind pure expressions to names, where pure means that the expression has no side-effects [3].
We can now just use the name src
instead of having to type checkAddress (addressLookup s) addr
every time.
Don't worry about efficiency, the evaluation mechanics of Haskell will ensure that the actual expression is only calculated once, even when we use the src
name multiple times.
In the next case-statement we check if the current or a remote memory manager is responsible for handling the address.
In either alternative we must use the do
keyword again because we will be executing multiple statements.
We will now finally use some of the API functions, the first we encounter is:
addrVal <- readMemory addr
The readMemory
function accesses the simulator environment, retrieving the value of the memory location specified by addr
.
We use the left-arrow <-
to indicate that this is a side-effecting expression (we are accessing the simulator environment), and that addrVal
is not bound to the expression itself, but the value belonging to the execution of this statement.
After reading the memory, we send the value back to the module that initially requested the memory access.
We send the read value as a response to the return address (retAddr
).
Having serviced the request, we use the yield
function to give the (unaltered) internal state back to the simulation environment.
If a remote memory manager is responsible for the address:
Just remote -> do
response <- invoke MemoryManager remote content
respond MemoryManager retAddr response
yield s
We then synchronously invoke the remote memory manager with the original read request, and forward the received response to the component making the original memory request.
The third alternative, handling a write request, is analogous to handling a read request.
In the situations which we didn't handle explicitly, such as receiving a Tick
, we simply disregard the simulator event, and return our unaltered internal state to the simulator.
ComponentInterface Instance
At the bottom of our MemoryManager
module we see the following code:
instance ComponentInterface MemoryManager where
type State MemoryManager = MemState
type Receive MemoryManager = MemCommand
type Send MemoryManager = Dynamic
initState _ = (MemState [])
componentName _ = "MemoryManager"
componentBehaviour _ = memoryManager
Here we define a so-called type-class instance.
At this moment you do not need to know what a type-class is, just that you need to define this instance if you want your component to be able to be used by the SoOSiM simulator.
We use our singleton datatype, MemoryManager
, as the label/name for our ComponentInterface instance.
All (type-)functions in this interface receive the interface label as their first argument.
For the type-functions (such as State s
) we must explicitly mention the label, for normal function we can just use the underscore (_
) as a place holder.
The instance must always contain the definitions for State
, Receive
, Send
, initState
, componentName
and componentBehaviour
.
The State
indicates the datatype representing the internal state of a module.
The Receive
indicates the datatype of messages that this component is expecting to receive.
The Send
indicates the datatype of messages this component will send as responses to invocation.
The initState
function returns a minimal internal state of your component.
The componentName
is a function returning the globally unique name of your component.
Finally componentBehaviour
is a function returning the behaviour of your component.
The behaviour of your component must always have the type:
(State iface) -> Input (Receive iface) -> Sim (State iface)
Where State iface
is the datatype of your component's internal state, and Receive iface
is the datetype of the type of messages the component handles.
SoOSiM API
ComponentInterface Type Class
-- | Type class that defines every OS component
class ComponentInterface s where
-- | Type of messages send by the component
type Send s
-- | Type of messages received by the component
type Receive s
-- | Type of internal state of the component
type State s
-- | The minimal internal state of your component
initState :: s -> State s
-- | A function returning the unique global name of your component
componentName :: s -> ComponentName
-- | The function defining the behaviour of your component
componentBehaviour :: s -> State s -> Input (Receive s) -> Sim (State s)
Simulator Events
data Input a
= Message a ReturnAddress -- ^ A message send by another component: the
-- first field is the message content, the
-- second field is the address to send
-- responses to
| Tick -- ^ Event send every simulation round
Accessing the simulator
-- | Create a new component
createComponent ::
(ComponentInterface iface, Typeable (Receive iface))
=> iface
-- ^ Component Interface
-> Sim ComponentId
-- ^ 'ComponentId' of the created component
-- | Synchronously invoke another component
invoke ::
(ComponentInterface iface, Typeable (Receive iface), Typeable (Send iface))
=> iface
-- ^ Interface type
-> ComponentId
-- ^ ComponentId of callee
-> Receive iface
-- ^ Argument
-> Sim (Send iface)
-- ^ Response from callee
-- | Invoke another component, handle response asynchronously
invokeAsync ::
(ComponentInterface iface, Typeable (Receive iface), Typeable (Send iface))
=> iface
-- ^ Interface type
-> ComponentId
-- ^ ComponentId of callee
-> Receive iface
-- ^ Argument
-> (Send iface -> Sim ())
-- ^ Response Handler
-> Sim ()
-- ^ Call returns immediately
-- | Respond to an invocation
respond ::
(ComponentInterface iface, Typeable (Send iface))
=> iface
-- ^ Interface type
-> ReturnAddress
-- ^ Return address to send response to
-> (Send iface)
-- ^ Value to send as response
-> Sim ()
-- ^ Call returns immediately
-- | Yield internal state to the simulator scheduler
yield ::
a
-> Sim a
-- | Get the component id of your component
getComponentId ::
Sim ComponentId
-- | Get the node id of of the node your component is currently running on
getNodeId ::
SimM NodeId
-- | Create a new node
createNode ::
Sim NodeId -- ^ NodeId of the created node
-- | Write memory of local node
writeMemory ::
Typeable a
=> Int
-- ^ Address to write
-> a
-- ^ Value to write
-> Sim ()
-- | Read memory of local node
readMemory ::
Int
-- ^ Address to read
-> Sim Dynamic
-- | Return the component Id of the component that created the current
-- component
componentCreator ::
Sim ComponentId
-- | Get the unique 'ComponentId' of a component implementing an interface
componentLookup ::
ComponentInterface iface
=> iface
-- ^ Interface type of the component you are looking for
-> Sim (Maybe ComponentId)
-- ^ 'Just' 'ComponentID' if a component is found, 'Nothing' otherwise
Handling Dynamic
Values
-- | Converts a 'Dynamic' object back into an ordinary Haskell value of the
-- correct type.
unmarshall ::
Typeable a
=> Dynamic -- ^ The dynamically-typed object
-> a -- ^ Returns: the value of the first argument, if it has the
-- correct type, otherwise it gives an error.
References
[1] Here is a chapter from a book that introduces the correspondence between Haskell types and C types:
http://book.realworldhaskell.org/read/defining-types-streamlining-functions.html
[2] Some resources that discuss monads: http://book.realworldhaskell.org/read/monads.html and http://learnyouahaskell.com/a-fistful-of-monads
[3] A more elaborate explanation of purity can be found here: http://learnyouahaskell.com/introduction#so-whats-haskell