From cross-platform to multiple platforms

A year ago, I announced that a cross-platform version of Enigma would be released. Since then, I’ve explored several scenarios. Finding a good programming language seemed simple at first: the most obvious choice appeared to be Go. An excellent language that is inherently cross-platform. However, there were two major challenges I needed to address:

  1. Accessing the Swiss Ephemeris (SE).
  2. Building a user interface.

The SE is written in C. You can integrate the SE’s C code into a Go application using an FFI (Foreign Function Interface). This incurs some performance overhead and requires converting data between C and Go, but it’s manageable. However, you’d need a C compiler available when compiling your program. This raised questions: Would it work on iOS and Android? Could it cause issues when submitting the app to the App Store?

To solve this, I attempted to rewrite the SE in Go using AI. While I believe it’s feasible, it would require an enormous amount of time—so I abandoned this effort.

Another issue with Go is the user interface. Many suggest using Flutter, but that typically involves running Go as a backend server. If you want a native approach, you’d still need an FFI between Go and Dart (Flutter’s language), which would handle all backend-frontend communication—a major drawback in my view. Additionally, Flutter is primarily aimed at mobile platforms, which I do want to support, but my current focus is on desktops.

An alternative for Go is Fyne, a UI framework written in Go. Fyne works well, but its capabilities are limited. Most Fyne examples showcase relatively simple applications. I began doubting Go and realized there was another option: C#, but using Avalonia instead of the standard WPF for the UI. Avalonia is an evolution of WPF, more efficient, and supports multiple platforms. C# is also cross-platform.

I noticed that a C#/Avalonia app on macOS runs with DLLs. The SE is also available as a DLL—but only for Windows. On macOS, you’d need to compile a dynamic library (a .dylib, macOS’s equivalent of a DLL) from the SE code. Even then, it’s unclear whether this would work on mobile platforms later.

Moreover, it remained uncertain how likely a C#/Avalonia app would be accepted into the App Store.

I made a list of my priorities:

  • A good user interface that feels native.
  • App Store acceptance.
  • No platform-specific issues.

There are other factors, of course, like performance and testability, but I’m focusing on the problems I encountered. My conclusion:

  • Avalonia offers a great UI but not a truly native feel for all platforms.
  • Fyne falls short in terms of features.
  • App Store acceptance is questionable for all alternatives.
  • I still can’t avoid platform-specific issues with these solutions.

As a result, I decided not to use these alternatives. Researching, experimenting, and building prototypes took too much time. So, I’ve adjusted my goals. I aimed for a fully cross-platform solution, but after extensive research and experimentation, I’ve realized this approach comes with too many compromises. Instead, I’ve decided to focus on native development first, prioritizing quality and platform integration over universal compatibility.

The revised plan:

  1. MacOS/iOS First: I’ll develop the core application in Swift/SwiftUI, ensuring optimal performance and seamless integration with Apple’s ecosystem. (Swift can access the C-code of the SE).
  2. Windows Second: The Swift code will be adapted to C#/Avalonia for Windows, reusing logic where possible while accepting some duplication. This will happen in parallel to the macOs development.
  3. Android and Linux: Android and Linux support will be reconsidered once the desktop versions are stable, likely requiring platform-specific solutions.

This shift means maintaining separate codebases, but it guarantees better performance, smoother UI, and fewer cross-platform quirks. The trade-off is worth it for a polished product.

Accessing the Swiss Ephemeris

The Swiss Ephemeris(SE) is the standard library for astronomical calculations in astrological software. The SE is written in Ansi C and is easy to use by programs written in C or C++. For software in other languages, a dll is available but this of course is only usable in a Windows environment.

My intention is to rewrite the core of Enigma in Go and the user interface in Flutter/Dart. This means that I need to access the SE using code in Go. Three options are available:

  • rewriting the SE in Go. This would be an enormous task.
  • using the DLL. This would restrict the usage of Enigma to a Windows environment and I want to move to a cross-platform application.
  • accessing the C-code of the SE via a FFI (Foreign Function Interface) solution. Fortunately, such a solution exists for Go. It’s called cgo.

Using cgo, I can use the original c-code in my application and call C-functions from Go. This approach does require converting Go-variables into their C counterparts and it has some limitations on constructions with pointers. And there is a performance penalty: the conversion takes time and calling a C-function from Go is much slower than calling a C-function from C.

How does this work out in practice? I can already tell you that you would not be able to see any difference for a chart-calculation. But it could be an issue if a large sequence of calculations is required, e.g. for cycles.

I wanted to be sure about this, so I built a small wrapper for the SE, using cgo. The wrapper supports two functions: initializing the SE and calculating the position of a celestial body. I used code from the existing Go wrapper by Kaveh Shahbazian (https://github.com/workshop-depot/gosweph ) but needed to make some changes. I used this wrapper in a small program that calculates the position of Sun, Moon and Mercury for 1,000,000 consecutive days. I also wrote a program with the same functionality in C#, using the standard dll from the SE. The following table shows the time these calculations took, measured in milliseconds.

Calculation forMilliseconds when using Go and cgoMilliseconds when using C# and the SE dll
Sun10,3518,935
Moon10,4028,784
Mercury11,4509,623

The results show clearly that the combination of C# and the SE dll is faster but the differences are pretty small, a bit over 15%. In both cases we are talking about roughly 100,000 calculations per second. My conclusion: there is a small performance penalty when accessing the SE with Go but it is negligible.

I performed the tests on a PC with Windows 11, an AMD Ryzen 9 processor and 32 GB internal memory.

Enigma cross-platform

Enigma is currently only compatible with Windows. For a long time, I assumed that the vast majority of astrologers used Windows. That the Windows share was declining, both due to the popularity of Apple and mobile platforms, was of course well known. But this process is moving faster than I thought. On June 25, 2024, I posted a survey in a popular Dutch Facebook group about astrology. My question: which environment was preferred for working with astrological software. 77 people responded, yielding the following score:

  • Windows (desktop/laptop): 51%
  • Apple MacOs (desktop/laptop): 10%
  • Linux (desktop/laptop): 3%
  • Apple iOs/iPadOs (smartphone/tablet): 25%
  • Android (smartphone/tablet): 11%

A survey like this is improvisational but you can glean some important conclusions from it:

  • half of the astrologers prefer Windows.
  • over a third work with Apple equipment.
  • and a third of astrologers work with Apple or Android mobile devices.

I draw the conclusion that it is insufficient to develop Enigma only for Windows. You can, of course, run Windows applications on an Apple computer via emulation software like Parallels. However, that is a stopgap solution and, moreover, it does not work on mobile devices. What you need is a cross-platform application, a program that works on desktops with Apple, Linux or Windows and also on mobile equipment with Apple or Android.

Realizing this will take a lot of time. So much time that for now I will continue to develop Enigma in the C# programming language, so only for Windows. In parallel, I am starting to convert to another environment that is cross-platform. At the latest by the time Enigma is ready for release 1.0, and the main functionality is built, I’ll make the switch. This will probably take a year or two.

Technical Choices

To move to a new way of working that supports cross-platform development, I need to make three important choices:

  • What will be the core programming language.
  • How do I approach the Swiss Ephemeris.
  • What techniques do I use to build the GUI.

Programming Language

I have looked not only at supporting cross-platform development but also at general aspects of programming languages. Almost always, I worked with object-oriented languages such as Java, Kotlin and C#. This had the advantage that I was better able to write larger and complex programs. But object orientation also has a disadvantage: you have to be very careful about over-engineering. I started looking for alternative languages and found two candidates: Rust and Go. Rust is a language that is especially suitable for critical applications and for applications that need to work in real time, like system software. I worked with Rust for a short time and found it to be a well-thought-out language. But Rust is complex and is known for its “steep learning curve”. Go is less rigorous than Rust but offers sufficient guarantees of reliable applications. Go is less suitable for time-critical applications because – unlike Rust – it works with a garbage collector. The big advantage of Go is that it is a minimal language. As a result, you can easily learn Go. Moreover, the small instruction set is pragmatic and you have a large amount of libraries you can use. Due to the lack of much syntax sugar, Go compiles very quickly and has excellent performance. The choice therefore is Go, with Rust as an alternative.

I do not consider languages like C and C++, even though they offer easier access to the Swiss Ephemeris (see next section). The chance of wrong memory allocations is very high in these languages; Rust would be a better alternative. Python is also not considered, in my opinion it is difficult to write large projects in Python, in addition Python offers much lower performance than native compiled languages.

Swiss Ephemeris

The Swiss Ephemeris (SE) is written in C. To access it from another language, there are three options:

  • a port: completely rewriting the SE in the desired language.
  • a wrapper: placing the original C code of the SE in a wrapper of the desired language.
  • JNI: Java Native Interface, a standard way to access compiled code from another environment.

Writing a full port is very much work but is possible. Thomas Mack wrote a port of the SE in Java, years ago, and it worked very well. But the SE consists of tens of thousands of lines of code, if you write a port you spend a lot of time both building and maintaining it: you also want to support future versions of the SE. A possible option but certainly not attractive.

A wrapper is easier to write, especially in Go and Rust. Wrappers in Go, that I can reuse, already exist. This seems like a useful solution.

JNI in itself is ideal but only works with languages that use the JVM: Java, Kotlin etc. Kotlin in particular is an excellent language but does carry the risk of over-engineering.

GUI

The current version of Enigma uses WPF for the GUI. WPF is complex but allows for a highly scalable GUI. Unfortunately, WPF is only suitable for Windows. An alternative like MAUI is still too immature and also does not support Linux GUI’s.

Broadly speaking, there are two alternatives:

  • an on-line application.
  • a web-based application, but packaged as a native application.
  • a cross-platform native library.

An on-line application has its problems: you would not have good capabilities for using a database and only limited capabilities for storing user preferences. Moreover, an Internet link does not give the feeling of an application: you build a relationship with the users to a lesser extent.

A Web-based native application does behave like a real application but requires the use of JavaScript; I see that as a major drawback. You can choose from platforms like Electron.js and Tauri. But Electron.js does not support mobile platforms. Tauri plans to do so in the future but is not yet that far. Tauri does require the use of Rust.

A well-known cross-platform native library is Flutter. Originally intended for mobile applications, Flutter now supports all relevant desktop variants. Flutter uses Dart, an object-oriented language. Dart is not suitable for the backend; for that you are better off using Go. Flutter and Go work very well together.

Conclusion.

  • Programming language backend: Go
  • Frontend programming language: Dart
  • GUI: Flutter
  • SE: wrapping.

I hope to regularly publish findings on the progress of this project. Enigma will remain open source, the code, including future cross-platform code, you can use in your own project, as long as your own code is also open source.

Dedicated Enigma modules

Enigma will support dedicated modules: programs to handle a specific research project but also usable for other projects. The code from the dedicated modules will often, but not always,  be added to the Enigma main program.

The first dedicated module is Enigma DedVM.  It supports a project by dutch researcher Vivian Muller into the astrological aspects of suicide.

You can download the compiled program and a user manual at radixpro.com. The source code is available at GitHub.

Back to Java

In august 2018 I decided to develop Enigma in Free Pascal. The main reason was that Oracle terminated the support for JavaFX. A company called Gluon would support it as an open source project, but I did not believe that JavaFX would last. Swing and SWT were no alternatives for me and using a web-interface in a native application had severe drawbacks.
I made some comparisons and Free Pascal was the clear winner. It was object oriented, open source and had an exceptional good way of handling the user interface. I knew the language from previous experiences, though that was some 15 years ago.
So I happily started coding. At first everything went well but as the application grew I experienced some stumble-blocks. The editor, with the beautiful name Lazarus, was good but it paled in comparison to intelliJ, the Java editor I was used to. Unit testing was possible but it missed some essential techniques, like mocking and calculating the test-coverage. And the way the source was handled, in units, files with mancy classes, was cumbersome. OK, I could have created small units but then I would have had problems defining the imports. I expect to be using at least 1000 classes after some further development, so this became a problem.
I found myself coding less and less because it was not so much fun anymore.
At the end of december 2019 the first version of Enigma was almost ready. It could calculate and draw a chart, work with configurations and used a database to store the essential information.
But I strongly felt the need for an alternative.
So I made a new comparison. Then I found that JavaFX was not dead. This was a big surprise for me. I expected it to slowly die after Oracle pulled the plug, but the open source community decided differently. JavaFX is alive and kicking and the support by Gluon works very well. Several new releases have been published while I was working with Free Pascal.
So I decided to return to Java. I was delighted with the efficiency of intelliJ and started coding. About 11 weeks later I almost completely rebuilt the codebase as writtin in Free Pascal. I am sure that future development will be fast as I’m using the perfect tools ànd coding is again fun for me.
I expect to have a first (beta) version, which will be 2020.1, available in May this year.
A new sourceset will then be posted at GitHub.