Chapter 7: Coding the Application

Richard Kenneth Eng
Learn How To Program
9 min readJun 4, 2017

--

As we are embarking on a section where we are interfacing with low-level operating system primitives, backing up your work is always important as you can potentially hang your environment and need to restart your computer. Fortunately in Smalltalk there are some great recovery mechanisms, one of which isn’t often found in a traditional programming environment, namely a coding changes log. This file contains all the coding operations you perform in Pharo giving you a useful recovery mechanism in an emergency. In Pharo 6, this file is augmented with a tool called Epicea which lets you filter these changes and re-apply them if needed. Equally, you can also use more conventional version control tools and Pharo supports two types: Monticello (a Smalltalk proprietary VCS), and more recently, Iceberg, a Git VCS utility. When using these VCS tools, you should frequently check-in your modified code, particularly before trying out your work. You can, of course, also simply backup the Pharo .image and .changes files periodically. For simplicity, I will describe the latter approach and suggest the following two-step approach:

  1. Copy Pharo.image to a backup location.
  2. File Out the #Cranky class. In the System Browser, right-click on the Cranky class to bring up the context menu and select ‘File Out’. This will create a file called Cranky.st.

If the image crashes, just copy back the backed-up image. In the worst case, you can rebuild the image by starting with a fresh Pharo image and importing Cranky.st (by dragging-and-dropping it into the Pharo window and choosing ‘FileIn entire file’ from the context menu.)

FFI class

Let’s start with the FFI class:

FFILibrary subclass: #FFICranky
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Cranky'

Normally, we would use a call like:

getBuffermem
^ self ffiCall: #( uint get_buffermem() ) module: 'ffilibc.so'

For reasons of “portability” (the C library may have different names under different operating systems) and brevity (no need to type ‘module:’), we subclass #FFILibrary.

So for any Linux (or Unix) system, we create a #unixModuleName method. If we wanted to run Cranky under Windows, we would have a #win32ModuleName method.

Now, we add the methods for calling into our C library:

unixModuleName
^ 'ffilibc.so'
getBuffermem
^ self ffiCall: #( uint get_buffermem() )
getCachedmem
^ self ffiCall: #( uint get_cachedmem() )
getCpuCount
^ self ffiCall: #( uint get_cpu_count() )
getCpuFreq
^ self ffiCall: #( uint get_cpu_freq() )
getCpuTemp
^ self ffiCall: #( float get_cpu_temp() )
getCpuUsage
^ self ffiCall: #( double get_cpu_usage() )
getFreemem
^ self ffiCall: #( uint get_freemem() )
getFreeswap
^ self ffiCall: #( uint get_freeswap() )
getFsAvail
^ self ffiCall: #( uint get_fs_avail() )
getFsTotalsize
^ self ffiCall: #( uint get_fs_totalsize() )
getMachine
^ self ffiCall: #( String get_machine() )
getNodename
^ self ffiCall: #( String get_nodename() )
getNumprocs
^ self ffiCall: #( uint get_numprocs() )
getRelease
^ self ffiCall: #( String get_release() )
getSysname
^ self ffiCall: #( String get_sysname() )
getTotalmem
^ self ffiCall: #( uint get_totalmem() )
getTotalswap
^ self ffiCall: #( uint get_totalswap() )
getUptime
^ self ffiCall: #( uint get_uptime() )
getVersion
^ self ffiCall: #( String get_version() )

(You can categorize these methods into protocols, like CPU, Memory, SD card, System, and Time. This makes it more convenient to look up a method. I leave this as an exercise for you.)

Since this class is a singleton, we want to ensure that there is one, and only one, instance of it…ever. A special class method exists for this, and it is inherited by FFICranky (which is why ‘super’ appears in place of ‘self’):

FFILibrary class»uniqueInstance
uniqueInstance ifNil: [ uniqueInstance := super new ].
^ uniqueInstance

Of course, there has to be a corresponding instance variable on the class side:

FFILibrary class
instanceVariableNames: 'uniqueInstance'

The logic of uniqueInstance is that if FFICranky hasn’t been instantiated (indicated by nil), then instantiate it now. Then return the instance object.

MyStringMorph class

In Chapter 6: Program Design, we wrote pseudocode suggesting that we perform an endless loop updating the relevant information fields in the application window. However, further investigation shows that this is not the wisest approach. The Morphic library is not thread-safe (meaning that it’s dangerous to use it from another thread).

Fortunately, Morphic provides an alternative which works fine. It lets you define a method that is executed periodically in order to update a morph. The classic example is a clock application.

We need to generalize #StringMorph so that we can pass in the code we need to update the morph. We begin by subclassing #StringMorph:

StringMorph subclass: #MyStringMorph
instanceVariableNames: 'block'
classVariableNames: ''
poolDictionaries: ''
category: 'Cranky'

We create an instance variable ‘block’ to hold the code that will update our morph.

Instances of this class should do the same initialization as in the parent class; this can be done by sending the initialize message to #super.

We also want a class method to allow us to pass in the update block. It will instantiate the object and call an instance-side method named #update: to set the value of ‘block’. This is a matter of convenience so that we don’t have to explicitly use ‘new’.

MyStringMorph class»update: aBlock
^ self new update: aBlock

And here are the instance methods:

initialize
super initialize
update: aBlock
block := aBlock
step
self contents: block value
stepTime
^ 2000 "in milliseconds"

The thing with #MyStringMorph is that as soon as it is instantiated, the #step method will immediately begin execution according to the timer determined by #stepTime. There is nothing more to do to get the morph to self-update periodically.

InfoLine class

Now, let’s define a class to represent a line of information (which includes the label) in our application window:

Object subclass: #InfoLine
instanceVariableNames: 'label info'
classVariableNames: ''
poolDictionaries: ''
category: 'Cranky'

As in MyStringMorph, we want class methods to allow us to pass in several arguments. We create class methods that call similar instance methods:

InfoLine class»label: label contents: con at: pos
^ self new label: label contents: con at: pos
InfoLine class»label: label update: aBlock at: pos
^ self new label: label update: aBlock at: pos

Then the instance methods are:

label: lab contents: con at: pos
label := ((StringMorph contents: lab) color: Color white)
position: pos.
info := ((StringMorph contents: con) color: Color white)
position: 100@pos y
label: lab update: aBlock at: pos
label := ((StringMorph contents: lab) color: Color white)
position: pos.
info := ((MyStringMorph update: aBlock) color: Color white)
position: 100@pos y

Note that the label and info fields are on the same line (y coordinate), and the info field is shifted 100 units over to the right (using a coordinate system where the origin 0,0 is at the top-left).

You must also create the accessor methods for ‘label’ and ‘info’…

info
^ info
info: anObject
info := anObject
label
^ label
label: anObject
label := anObject

(You can actually do this more easily by going into the context menu for #InfoLine and selecting Refactoring→Inst Var Refactoring→Accessors.)

Cranky class

Finally, we can construct our application screen with the required labels and information fields. We need a number of instance variables, such as the main window morph (‘m’), and 19 information lines (each consisting of a label and an info field).

Object subclass: #Cranky
instanceVariableNames: 'm s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19'
classVariableNames: ''
poolDictionaries: ''
category: 'Cranky'

Just for convenience, we write methods to construct different portions of the screen according to the screen layout we had in Chapter 6. (I threw the Uptime morph into #initSystemfields just for convenience; it’s hardly worth a separate method for it.)

initSystemfields
m addMorph: ((((StringMorph contents: '== System ==')
color: Color white) position: 0@75)
emphasis: 1).
s1 := InfoLine label: 'System name:'
contents: FFICranky uniqueInstance getSysname at: 0@90.
s2 := InfoLine label: 'Node name:'
contents: FFICranky uniqueInstance getNodename
at: s1 label position + (0@15).
s3 := InfoLine label: 'Release:'
contents: FFICranky uniqueInstance getRelease
at: s2 label position + (0@15).
s4 := InfoLine label: 'Version:'
contents: FFICranky uniqueInstance getVersion
at: s3 label position + (0@15).
s5 := InfoLine label: 'Machine:'
contents: FFICranky uniqueInstance getMachine
at: s4 label position + (0@15).
m addMorph: ((((StringMorph contents: '== Time ==')
color: Color white) position: s5 label position + (0@30))
emphasis: 1).
s6 := InfoLine label: 'Uptime:'
update: [FFICranky uniqueInstance getUptime asDuration
humanReadablePrintString]
at: s5 label position + (0@45).
m addMorph: s1 label; addMorph: s2 label; addMorph: s3 label;
addMorph: s4 label; addMorph: s5 label; addMorph: s6 label;
addMorph: s1 info; addMorph: s2 info; addMorph: s3 info;
addMorph: s4 info; addMorph: s5 info; addMorph: s6 info
initMemoryfields
m addMorph: ((((StringMorph contents: '== Memory ==')
color: Color white)
position: s6 label position + (0@30)) emphasis: 1).
s7 := InfoLine label: 'Free mem:'
update: [FFICranky uniqueInstance getFreemem
asStringWithCommas,' KB']
at: s6 label position + (0@45).
s8 := InfoLine label: 'Total mem:'
contents: FFICranky uniqueInstance getTotalmem
asStringWithCommas,' KB'
at: s7 label position + (0@15).
s9 := InfoLine label: 'Cached mem:'
update: [FFICranky uniqueInstance getCachedmem
asStringWithCommas,' KB']
at: s8 label position + (0@15).
s10 := InfoLine label: 'Buffer mem:'
update: [FFICranky uniqueInstance getBuffermem
asStringWithCommas,' KB']
at: s9 label position + (0@15).
s11 := InfoLine label: 'Free swap:'
update: [FFICranky uniqueInstance getFreeswap
asStringWithCommas,' KB']
at: s10 label position + (0@15).
s12 := InfoLine label: 'Total swap:'
contents: FFICranky uniqueInstance getTotalswap
asStringWithCommas,' KB'
at: s11 label position + (0@15).
s13 := InfoLine label: '# of processes:'
update: [FFICranky uniqueInstance getNumprocs asString]
at: s12 label position + (0@15).
m addMorph: s7 label; addMorph: s8 label; addMorph: s9 label;
addMorph: s10 label; addMorph: s11 label; addMorph: s12 label;
addMorph: s13 label; addMorph: s7 info; addMorph: s8 info;
addMorph: s9 info; addMorph: s10 info; addMorph: s11 info;
addMorph: s12 info; addMorph: s13 info
initSdCardfields
m addMorph: ((((StringMorph contents: '== SD card ==')
color: Color white)
position: s13 label position + (0@30)) emphasis: 1).
s14 := InfoLine label: 'Free space:'
update: [FFICranky uniqueInstance getFsAvail
asStringWithCommas,' MB']
at: s13 label position + (0@45).
s15 := InfoLine label: 'Total space:'
contents: FFICranky uniqueInstance getFsTotalsize
asStringWithCommas,' MB'
at: s14 label position + (0@15).
m addMorph: s14 label; addMorph: s15 label; addMorph: s14 info;
addMorph: s15 info
initCpuFields
m addMorph: ((((StringMorph contents: '== CPU ==')
color: Color white)
position: s15 label position + (0@30)) emphasis: 1).
s16 := InfoLine label: 'CPU usage'
update: [(FFICranky uniqueInstance getCpuUsage
printShowingDecimalPlaces: 1),'%']
at: s15 label position + (0@45).
s17 := InfoLine label: 'Frequency'
update: [FFICranky uniqueInstance getCpuFreq
asStringWithCommas,' MHz']
at: s16 label position + (0@15).
s18 := InfoLine label: 'CPU/GPU temp:'
update: [(FFICranky uniqueInstance getCpuTemp
printShowingDecimalPlaces: 1),' C']
at: s17 label position + (0@15).
s19 := InfoLine label: '# of cores:'
contents: FFICranky uniqueInstance getCpuCount asString
at: s18 label position + (0@15).
m addMorph: s16 label; addMorph: s17 label; addMorph: s18 label;
addMorph: s19 label; addMorph: s16 info; addMorph: s17 info;
addMorph: s18 info; addMorph: s19 info

(The humanReadablePrintString message is very handy for displaying duration [in seconds] as hours, minutes, and seconds.

The emphasis: message is used to set bold or italics or underline, using the values 1, 2, 4, respectively.)

In each of these methods, we’re instantiating each information line from our screen layout and adding the morphs to our main application window. Some of the morphs are simply headers.

Each information line is about 15 coordinate units high. Note that the positions of the morphs depend on the position of the preceding morph. This makes it easy to shift all the morphs as a group by simply changing the position of the topmost morph.

To create the main application window, we use the hot air balloon image as a background for a windowed ImageMorph. To do this, you create an instance of #Form from the balloon image file, then set it as the image in the newly created ImageMorph using the form: message.

initialize
| f |
f := Form fromFileNamed: 'hot_air_balloon_mysticmorning.jpg'.
m := ImageMorph new.
m form: f.
self initSystemfields.
self initMemoryfields.
self initSdCardfields.
self initCpuFields.
(m openInWindowLabeled: 'Cranky') location: 0@0 "place in top-left corner of Pharo window"

When you perform ‘Cranky new’ in the Playground, you should see something like this:

Congratulations! You now have your first working Pharo application.

--

--