DEV Community

Chris Riddick
Chris Riddick

Posted on

How One Experienced Software Engineer Learns a New Programming Language

I retired two years ago from a software engineering career that started in 1979, but have never lost the joy of programming and building useful applications. I recently decided to challenge myself to learn a new programming language just for the fun of it. My most recent experience had been with Java and Linux doing big data processing.

Java is a cool language. It can do most anything. it is also large and complex. It can take years to master even the most basic features. It is also fully object-oriented with all of the baggage that goes with OO. My first job years ago was writing assembly language programs for Z80 on a custom data collection system. We were doing FFTs and other digital signal processing with only the Z80 and a multiplier chip from Texas Instruments. I loved it!

I want to relive that experience with a new language. I've used C, Objective-C, FORTRAN, Java, Python, and dozens of other specialized languages. What I really enjoyed about assembler and the C language was the simplicity and purity of the instruction set and features. Was there something that I could learn that got me back to my roots and closer to the basics?

I learn best when I'm building something useful, not just working tutorials or examples. I wanted to find a new language to learn that would enable me to build something other people might actually find useful or fun -- maybe something to help others to learn, too. Working my way down the list of popular languages, I narrowed my choices down to two: Rust and Go. These are languages people are using to build applications today. Rust is even making inroads into the Linux core and kernel, showing off its strength at building complex machine-level features. Go, or Golang as it is often known, is very C-like in that is doesn't have a lot of complex language features. Instead, it gives programmers enough tools to build anything, without overwhelming them with complexity. Unlike C, Go relieves us of memory management for garbage collection and helps avoid other issues related to data types and error handling. Surprisingly, Go has built-in testing and logging, too!

I decided on Go as my new language. I felt it was simple enough that I could learn the basics and begin building useful applications in a short amount of time. I also liked that it has a well-developed toolset for compiling and managing dependencies. If you've ever struggled with Maven or Gradle for Java, you know what I'm talking about.

I wanted to learn Go by designing and building an application that others may find useful. I've always had an interest in CPU (Central Processing Unit) design and instruction sets. I'm dating myself, but during my college days, I built an RCA Cosmac Elf from a Popular Electronics article. It was designed around the RCA 1802 8-bit microprocessor and 256 bytes of RAM. I was able to program it using eight switches and a button to input a machine language program that turned an LED on and off. You can't get any more basic than that.

Learning the RCA 1802, Z80, and other processors, I felt that I had a solid understanding of what was happening under the cover of any computer. It enabled me to be more successful in designing and building applications. It also helped me to debug and troubleshoot because I knew it was not "magic" behind the code, but very basic instructions you could follow. After working on a project that used a Motorola 68000 16-bit processor to simulate a Zilog Z80 8-bit processor, I realized way back then that you can simulate anything in a computer if you understand it and have the right tools.

I decided my learning project would be to develop a processor simulator in Go that could be used to teach the basics of CPU architecture and instruction sets. As an experience engineer, I wasn't trying to reinvent the wheel. There are thousands of open-source processor simulators out there. I decided to find an existing project in the public domain that was simple enough that I could take it and recode it to run in Go. I also wanted to add a graphical user interface (GUI) that would make it easy for a user to see the operation of the CPU instructions and to write simple machine-language programs to test ideas.

My approach to learning Go was first to read The Go Programming Language by Donovan and Kernighan and then peruse numerous resources like the website go.dev. Once I had a basic familiarity with the syntax and features of the language, I searched for code examples to see how other developers used the language. Since my interest was in CPU simulation, I searched for those examples.

Google and github have a wealth of information, and I quickly found a project that met my needs. The one I found was already written in Go, so I could see how a Go program is actually constructed. It was ritten by an engineer (Wojciech S. Gac) for a company competition . He posted his code on github here go-cpu-simulator. It had no GUI, so it gave me an opportunity to take someone else's work and add on a GUI written in Go. The project I selected was a simple CPU simulator with just eight instructions. It was a very basic CPU, but didn't require digging too deeply into someone else's code to figure out how to use it. I just needed to figure out how to add a useful GUI front-end to drive the simulator as an educational tool.

Reading someone else's code is not always easy. It helps if you can find a complete application to build and run while studying the code. Using Wojtek's simulator, I was able to see many of the features of Go in action.

Go is very C-like in the syntax. Like C, Go is simple and clean. There is not a lot of fluff in the language. Scope tends to be one area that causes confusion and trouble when learning a new language. Go is fairly straightforward in this regard. Modules and packages are clear concepts to grasp. If you want a variable or function to be visible outside a package, you capitalize the name. The Go language tools help you avoid scope errors by immediately warning you of names out of scope and unused variables and functions. Like C, Go uses structures. Go adds some basic object-oriented (OO) features by allowing methods on structures. Although not fully OO, Go's use of structures and methods expands the options of a programmer when designing application code.

Goroutines and channels give you simple, built-in concurrent processing without having to drop down into the operating system threading and processes.

Once I gained some familiarity with the simple CPU simulator by Wojtek, I began researching cross-platform graphical user interface frameworks for Go. My objective was to strap on a GUI front-end to his simulator so I could watch the simulator operate in real-time.

The GUI challenge

With a little research, I found a GUI framework that runs under Linux, macOS, and Windows. I selected the open source fyne.io framework, itself written in Go. It is cross-platform and has interfaces with Linux, Mac, and Windows so that any code I develop can be compiled for all three platforms for similar GUI results. It has enough capability that I felt I could create the GUI dashboard for running the simulator.

I wanted to build the dashboard in a way that could be generalized and reused for other CPU simulators I might want to build. Therefore, I wanted the dashboard to be its own module and package, separate from the simulator code. I used a Model-View-Controller pattern for my project. The Wojtek simulator was a single Go file. I created separate packages for the CPU (model), the dashboard (view), and the main program (controller). Using Go's module feature to manage external dependencies, I set the file structure with the main package and its go.mod dependency file and two folders, one for the dashboard package and the other for the cpu model package. Each folder represents its own module and has its own go.mod file. These can all be seen in my github repo.

The process of separating the program into three packages of main, dashboard, and simplecpu forced me to understand the fundamentals of Go scope and naming. It also clarified for me how the dependency system worked. My most recent experience has been with Java and Maven. I found the Go modules and dependency system simpler to work with compared to the complexity of Maven or Gradle.

The CPU monitor dashboard layout was fairly straightforward using the fyne.io framework. Like most GUIs, you create all your display objects and widgets, add containers for structuring the objects in columns, rows, and grids, and then place the containers into a window. I set up some control buttons with associated functions that get invoked when they are pressed. I also set up some label widgets to display specific CPU fields and data. I decided that it would be simpler for displaying memory if I pre-formatted memory in blocks of strings before placing them in containers. I created an UpdateAll() function that the controller called whenever it had new data to display.

The main package calls a dashboard.New() function from the dashboard package to create a new dashboard with all of its widgets and containers. The dashboard.New() function returns a pointer to a Fyne window object which the main() function owns. The call to the dashboard.New() function passes into the dashboard package a pointer to the CPU structure and pointers to all of the callback functions that the dashboard should run when buttons are pressed. Thus, the main package can control how the GUI responds to user input.

One thing I quickly learned about Go and threads is that as soon as the window object is created and displayed, it is operating independently from the main package. The button callback functions are how the GUI handles user input. A user presses the Run button and the CPU simulator begins executing instructions in the callback function. The callback function does not return to the dashboard window until the function ends. If a user tries pressing another button, like Pause, there is no response from the dashboard because it is blocked until the callback function for the Run button returns. I needed some way to allow user input while a CPU simulation is in progress. That's where Goroutines and channels come into play.

To prevent the dashboard from blocking all user input when a function is running, I need to give the button a way to run concurrently with the dashboard. The solution is to invoke the actual CPU execution using a Goroutine call in the callback function. As soon as the go Run() call is made, the code kicks off a separate Go thread running that function, returning control back to the main program and its dashboard.

Now, how do I let the program know when the Run() function is finished? I use a channel. Channels are simple, program-level, message-passing services that support concurrency. I set up a channel for each Goroutine, e.g., the Run() callback function, to send a message to a queue when it finishes. I set up a channel monitor function that runs as a Goroutine on the main program thread that watches for a message on that channel. When the message is received, the monitor function completes the processing necessary for the main thread to handle the results of the simulation. I end up with monitors for each button that could be pressed by a user so that the button can take as long as it needs to complete its task without blocking the dashboard from further user input.

Here is a screenshot of the dashboard running a simple program loaded in memory at address x0000.

Dashboard running a sample program on macOS

Expanding the CPU functionality and learning Go types and operations

Since the basic simulator was created to execute a limited set of seven instructions as part of a programming contest, it didn't spend a lot of time worrying about internal data formats and manipulation. The original CPU had a set of 17 integer (int) registers and a separate integer (int) stack array. Programs were byte (uint8) arrays. To run a program, the controller had to send a pointer to the code array to the processor and tell it how long the program was (length of the array). All operations were in native integer format.

When I built the dashboard and began monitoring execution of the sample programs, I realized that the simulator was not handling arithmetic the way a hardware CPU would have handled it. We had a mix of bytes and native integer format. Integer on some machines is signed, 64-bit. That's not what we really want to be simulating. We are simulating 16-bit registers operating on 8-bit bytes from a memory array.

I refactored the simplecpu package to use uint8 and uint16 variables to restrict it to what a simple hardware CPU might be doing. I moved the programs into memory as a sequence of bytes, and memory itself is simply an array of bytes (Big-Endian and Little-Endian storage is an important element in processor design). Rather than being a separate integer array, the stack is now a part of the memory, located by the 16-bit stack pointer. Programs execute beginning at wherever the program counter is pointing. Programs either run over the end of the memory array generating a memory fault, or they execute a HALT instruction to stop CPU processing. Now, the CPU simulator looks and acts like a basic 8-bit microprocessor from the 1970's. The dashboard displays the contents of memory and registers as the CPU executions instructions, and the user can observe how machine code runs. [Go arrays and slices are very important!]

The fyne.io framework provides an API for data binding so that data associated with a widget is automatically updated in the container without requiring the programmer to watch for changes and call update routines. Unfortunately, this application uses some more limited data types like uint8 and uint16 which are not currently supported by fyne.io. Thus, I had to use my own update function whenever a data field changed.

Another change I made was to the CPU package. As originally designed, a CPU instruction was completely contained in an 8-bit byte with the upper three bits defining the operation and the lower five bits as the operand. I wanted to be able to simulate a full 8-bit memory array with corresponding addressing, subroutines, and comparisons. To do this, I added an extended instruction set to the basic CPU, giving the simulator a more advanced programming capability while maintaining backward compatibility with the original seven instruction CPU. This is similar to how the original microprocessors like the 8008 and 8080 implemented backward instruction compatibility as technology progressed.

Simple CPU Simulator is far from finished. There are still some quirks in the GUI. I'm not sure I've mastered the concurrency feature using channels. I broke the go tests when implementing a clock-based execution loop. Programs have to be loaded by storing them in the code itself. It needs a feature to read an executable binary from a file.

This simple CPU simulator can be the basis for learning how real microprocessors operate and can help programmers better understand the internals of a CPU, the basics of machine-level arithmetic, and the use of memory in computers. Understanding the internals of the machine you program can give you better insights into its capabilities and limitations. it can also help you build more efficient applications.

Stay tuned for more as I integrate RCA 1802 and Z80 CPUs with the dashboard in future projects while continuing to learn Go and Fyne.io.

Project Summary

My code is maintained using github and is located here https://github.com/cjr29/go-cpu-simulator.git. This is a fork of Wojtek's repo. Wojtek is aware of my GUI and other enhancements. My project will be moving in other directions as I continue my learning, but I want to be sure he gets credit for providing a solid foundation for starting my learning.

Thanks for reading my post. Please let me know if this was helpful in the comments section. I'm happy to answer any questions.

Top comments (0)