This project…
This was inspired by this youtube playlist.
Being able to compare the primitive datatypes in Java and the equivalent in C++ shows that despite Java being a statically-typed language with lots of functionality, C (or C++ in this case) allows us to get a lot closer to the underlying Assembly Code (and therefore the CPU) than Java lets us get to the bytecode that runs on our JVM.
The C++ emulator makes a lot of use of the unsigned char
to replicate a byte, and an unsigned short
to replicate a word. Looking again at the primitive datatypes in Java, we have byte
as an 8-bit signed two's complement integer and we have a short
which is 16-bit signed two's complement integer.
However, one issue with Java is that there is limited ability to access the primitive binary datatypes. This page from Oracle shows a way of declaring binary values explicitly, but this is is a bit fiddly and makes the code harder to read. So, we’re using type-casting here from integers to bytes or shorts.
However, this is not too much of an issue. If we look at the two following classes ByteConversion
and IntegerDeclaration
- and then let’s see how the Java compiler translates this into Java bytecode.
class ByteConversion {
public static void main(String[] args) {
byte testByte = (byte) 1;
}
}
class IntegerDeclaration {
public static void main(String[] args) {
int testInt = 1;
}
}
The only difference between these two classes is that the variable declared in ByteConversion
is testByte
- where we’re typecasting a byte from an int (1 - 0000001 → 0001 for any anyone keeping count), whereas in IntegerDeclaration
we just declare the int value of 1.
If we use the javap
command (with the -v
flag to give us more details) we get the following information about how Java compiles this into bytecode.
javap -c ByteConversion.class
Compiled from "ByteConversion.java"
class ByteConversion {
ByteConversion();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: return
}
Then, if we do the same for IntegerDeclaration
:
javap -c IntegerDeclaration.class
Compiled from "IntegerDeclaration.java"
class IntegerDeclaration {
IntegerDeclaration();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: return
}
So, whilst the code is perhaps a little less user-friendly or readable, there is no computational tax for the type-casting. As we’re only using bytes to house 0s or 1s, this works fine in this case. We’ll assume the short datatype works in the same manner, but we’ll look at that later.
With Java's use of signed byte
and short
datatypes, some bitwise operations are needed to ensure that all variables are read as they would be by the 6502 processor.
Let's break down the operation:
address is a short, which means it's a 16-bit value. 0xFFFF is a hexadecimal value that represents 16 bits all set to 1. In binary, it's 1111 1111 1111 1111. When you perform a bitwise AND operation between address and 0xFFFF, each bit position is compared, and if both bits are 1, the result will be 1; otherwise, it will be 0.
Here's an example of how the operation works in binary terms:
address: 1101 0010 0100 1011 (example short value)
0xFFFF: 1111 1111 1111 1111
AND: 1101 0010 0100 1011 (address)
1111 1111 1111 1111 (0xFFFF)
------------------
1101 0010 0100 1011
Result: 1101 0010 0100 1011
As you can see, the bitwise AND operation preserves the least significant 16 bits of the address value and discards any bits beyond the 16th bit. This effectively ensures that the resulting value is within the valid range for your array index, which is what you want to achieve to avoid the ArrayIndexOutOfBoundsException.
In Java, primitive data types like int are passed by value, which means their values are copied when passed to a method. As a result, changes to the parameter within the method do not affect the original value outside the method.
So, in the CPU
class, there were methods that tried to increment the value of clock cycles down:
cycles--;
However, this infact did not increment down cycles
and led to an infinite loop.
The quick solution was to move it to after the switch case clause, with the slower solution being to create a mutableInt class.
One of C++'s great advantages is it's use of pointers. // for J - maybe cut this
One of the key differences (as much style as functionality) is the need to break out the consitutent parts of the CPU into various classes.
Given the ability for the C++ emulator to return a byte after a Boolean statement, we’ve used our own classes for byte
and short
(ByteMemory
and WordMemory
). This allows us to take advantage of polymorphism with our constructors - and pass bytes, ints or booleans and still get the desired outcome.
- add tests for negative bytes/shorts
- analyse byte code for binary bitwise operations