While Rust is not an object oriented language, it offers an appealing path to port libraries from Java. I recently completed porting the zxing library from Java to Rust, and I learned a lot about moving between the languages. I think many of these lessons can be applied to any object oriented language, but realistically porting from C++ to Rust is still radically different (and likely will be the subject of a later post).
Interior Mutability
Before we talk about anything else, we need to discuss the single largest stumbling block when porting anything from Java: Interior Mutability. If you aren’t familiar with the concept I will briefly summarize it: a type has interior mutability if its internal state can be changed through a shared reference to it. Interior mutability is fundamental to many object oriented systems, it allows shared references to be modified. In fact, nearly everything in Java (importantly, this includes arrays) is passed by reference to all methods and functions.
Imagine a “window” object in a GUI application. You might want to have a reference to that object available to many different (potentially simultaneously running) functions. You also might want to make changes to this object from multiple places. In rust, this concept is much more complicated.
A common pattern in Java is to pass a reference to an object to a function and allow that function to modify it. In bigger systems, things like cached values are a great example of this. In rust, shared mutable references are not permitted.
Solving Interior Mutability
There is not one single solution. That’s probably obvious, but it needs to be said. There simply isn’t a one size fits all way to resolve the issue. Each time I have found an interior mutability situation I have pulled different tools out to solve it, not because I like complication, but because there are different benefits and tradeoffs to each.
RefCel<T> is a popular option, and could be used for many situations where interior mutability exists, however I don’t believe that it’s the best option. If one were trying to literally port the exact API from Java to Rust, then RefCel would be unavoidable, but it’s not the best pattern when dealing with Rust, partially because you loose many of the memory and access protections for which Rust is famous.
In Java, many classes will have functions which modify the values of the object. While this is possible in rust, having every function be &mut self
would likely be an unappealing solution. Instead, I often found myself using functions instead of methods and passing in the struct that needed to be modified. That is something that comes up a lot in porting from Java to Rust, many methods in Java actually make more reasonable functions in Rust.
Java is happy to let you have as many mutable references to an object as you might want. Linked list types are very common in Java, partially for this reason. In many cases the only way I could find to port these types to Rust was using the Rc<T> type, which allows counted references to a value to exist in multiple places at once.
Another common Java pattern is a cached value for a complex calculation. if this.something == null { this.something = GenerateThing() } else { return this.something }
, trying to do this in rust without every single method being &mut self would have been impossible. Fortunately OnceCell solves this problem without extremely complex and unwieldy mutability patterns.
Static Classes
Most static classes in Java are actually pretty easy to represent as a module. While you don’t have the benefit of the static fields present in Java classes, a module with a little refactoring to accept parameters works nearly as well in all cases. This is one of the simpler porting scenarios between the two languages. While they look very different, they are actually very similar.
Interfaces
Java Interfaces are a lot like Rust Traits. I know that Rust Traits are not the same as a Java Interface, and I know that Rust structs are not the same as Java Classes, but it doesn’t matter. A Trait and an Interface are similar enough that one can refactor somewhat easily.
Function Overloading
Rust does not have function overloading. Whenever it is used in the original Java it is necessary to refactor the Rust solution in some way. Most of the time, I have found that having additional functions with a cascade calling starting with least complex and moving to most complex, handling default values between them, is the simplest. Often, I think the result is more readable, having a function named “doTask” which might accept one, two, or four variables and behave differently depending on what is passed seems less readable than having three functions with names which make it explicit what they do and what they expect.
Abstract Classes
These turn out to be particularly difficult to deal with since they can contain data and methods that works on that data. Rust doesn’t really have a concept like an uninstantiatable struct with implementations that can be re-used. It’s possible you could get around this by using custom Derive macros, but I don’t think that added complexity will usually be worth it. In rxing, I ended up turning all Abstract classes into traits and then having getter/setters for the data that the abstract class expected to have. It was a compromise, but it worked.
Inheritance
Inheritance does not exist in rust, and so is a very complicated issue when porting. When porting rxing, I could have possibly found a crate to make it simpler, but I thought that would likely end up being a long term maintainability issue, and adding another dependency wasn’t my favorite option. A good example of where this was a real mess was in the one dimensional module (oned). In the original java there is a neat class hierarchy working from an abstract one dimensional reader all the way down to specific implementations. Sometimes this was one step, sometimes multiple (an example: GenericOneDReader -> GenericUPCEANReader -> UPCReader -> UPCEReader). That was not neatly do-able in rust, so in that case I ended up using a custom Derive macro to handle generating the boilerplate code that would have been part of the class hierarchy.
In Conclusion
Porting Java to Rust is not a simple process. Rust is not an object oriented language, but that doesn’t mean that many concepts aren’t useful to understand. Where the process becomes complicated is when the source library relies heavily on OO features such as Inheritance or Abstract Classes. These concepts simply do not translate to Rust, and require a rethinking of how they are implemented, handled, and addressed. Doing this at the beginning of a project is wise, though not fool proof. As I mentioned above, there is no single solution to fixing any of these problems, and in different situations different methods might be more useful. It’s important to keep in mind that readability and code quality are linked, and so picking the best solution often means picking the most understandable solution. We hear, often, that we shouldn’t prematurely optimize code, and this is yet another situation where that is true.
Leave a Reply