[ADVANCED] Binary in Luau: Tutorial on Basic Computer Logic & Bit32

Hey Developers!

Have you ever stumbled by the Bit32 Library and wondered what it is for? Well this tutorial will help you understand the Binary System, Bit32 functions and how & where to use them.

1 - The Binary System

How to read Binary

In Binary, numbers are represented in bits, that can either be on or off (also known as 0 and 1).

Bits by themselves aren’t very useful and so they are often grouped into bytes, which is made up of 8 bits and can represent 256 different values.

This system of counting that bits work on is the base-2 method, where each bit is the value of all the bits before it + 1.

An example of this, lets say you got a byte of data. You want to find the Value of it, but how do you do it?

Here is a simple trick: The Value of the Bit is exactly 2x the value of the bit before.

So the 1st bit (bit 0 as it is called) would be worth 1, so work out what this value would be:

0000 0001

of course, it is 1. Now here is how we would represent 42

0010 1010

Remember, the Value of each bit = 2^n, in this case, n is the order of the bit.

Bit 0 is worth 1 as 2^0 = 1,
Bit 3 is worth 8 as 2^3 = 8,

So to find out if the binary sequence above was actually 42, we would sum all of the bits:

(2^0)*0+
(2^1)*1+
(2^2)*0+
(2^3)*1+
(2^4)*0+
(2^5)*1+
(2^6)*0+
(2^7)*0+

Adding all those numbers together would leave you with 42.
Look at the calculation more carefully, read it from top to bottom and you will notice something: on the right side of the calculation, the sequenece is equal to 0010 1010, which was our original number.

If you are converting from BinarySequence to lua, you could put 0b as a prefix, like: 0b00101010 or you could use the simpler but worse way of tonumber(00101010,2) or whatever your sequence is, it will return it in non-binary form (in this case: 42).

Remember: a byte’s value goes from 2^0 to 2^7 instead of 2^1 and 2^8 as the first value (bit 0) is worth 1, and the last value in a byte (bit 7) is worth 128.

If the Sequence was 1111 1111, it would be 255; the size of a value in RGB.

A faster way to calculate how large of a number can fit in a binary sequence, just do (2^n)-1, in this case, n is the amount of bits in a sequence, which here is 8.

Binary Addition

Binary Addition is easier than it seems.

Lets say you want to add 15 to 23, first of all we need to get the Binary Sequence of each number

15 would be: 0000 1111
23 would be: 0001 0111

Just to explain it, lets add 1+1.

0000 0001
0000 0001

If both values are 1, then we would carry 1 to the next digit and put the current digit as 0.

0000 0001
0000 0001
________
0000 0010

Which of course would equal to: 2, which is correct.

So lets continue with our previous equation: 15 + 23.

0000 1111
0001 0111

On bit 0, both values are 1, so we will carry:

       1
0000 1111
0001 0111
0000 0000

Here the carry and both values are 1. So we would carry 1 to the next bit and add the carry, which would also be 1 and a carry.

      11
0000 1111
0001 0111
0000 1110

Lets speed up the process and get straight into the answer:

   1 111
0000 1111
0001 0111
_________
0010 0110

The result is 0010 0110, which is 38, which is 15+23.

Binary Subtraction

If you understood binary addition, this would be no challenge for you!

In Binary, there are many ways to define negative numbers, such as having the last bit be worth 0 - it’s original value, also known as opposite.

But I do not want to overcomplicate things and so for now, we will stick with non-negative integers.

Lets have 2 Values that we want to subtract from:
9 - 5, or in other words:

  0000 1001
- 0000 0101
------------

Here it works differently,
If both numbers are 1, the output is 0.
If bottom number (the one we are subtracting the top number by) is 1 and top number is 0, it will carry to the next active bit. But instead of adding on to it, it would have that bit shifted right (halfing it)

By shifting to the right I simply mean this:

0010 1000 -> 0001 0100

But instead of the entire thing, we would just shift that bit.

Continueing with our example, lets do bit 0, which at this point you should know that it is the first bit.

0000 1001
0000 0101

In this case, it is basically 1-1, which is 0.

0000 1001
0000 0101
0000 0000

Bit 1 is empty on both sides so we will skip it.

Bit 2 is where it gets interesting, since the top number is false / 0, so we would search for the next bit,

We find our next active bit on Bit 3.

We shift Bit3 to Bit2, cancelling the 1 in the bottom sequence at bit2.
With it shifted, the calculation is more like this:

0000 0101
0000 0001

Apart from that we do not actually change the inputs, it is just to show the shift.

So that leaves us with:

0000 0101
0000 0001
0000 0100

0000 0100 equals to 4, which is 9-5.

Be Careful!
Once shifting a number, all numbers between the bit right of the bit being shifted and the bit that caused the shift would be activated!

In our case, the Bits were right next to each other, so only one was activated. But here is an example of one that would do that.

0000 1000
0000 0001

This is 8-1, but if we just shifted it to the right, then it would be

0000 0100
0000 0000

which would equal 4.

So instead we fill every bit in between The subtraction bit and The shifted bit’s new positon:

So it would be:

0000 0111
0000 0000

Which would equal to 7, which is our answer:

2 more examples:

0001 0010 -- 18
0000 0011 -- 3
-----------
0000 1111 -- 15
0010 0000 -- 32
0000 0001 -- 1
----------
0001 1111 -- 31

2 - Bit32 Library

In lua (The programming language that the Roblox Engine uses) has a Bit32 Library, that is useful for bitwise operations.

Here are the bitwise operators:
.band &
.bor |
.bxor ~
.bnot ~
.rshift >>
.lshift <<

instead of refering them as & or |, I will instead be using bit32.band and bit32.bor, to prevent any confussion.

As you can tell by the name, bit32 has 32 bits, not 8 bits that I used in the examples in Binary Arithmetic.

This allows the numbers to get crazy large, the limit is (2^32)-1 or 4294967295, that is 4.29 Billion!

Bitwise Operators

AND

The AND operator (bit32.band) returns the bits that are 1 in both A and B and returns it as an unsigned number.

Example:

local AND = bit32.band(2,3)
-- The 'AND' Variable would equal to 2, because:

-- 0010 (2)
-- 0011 (3)

-- they both have bit 1 as true, so the output would be 0010, which is 2. 

OR

The OR operator (bit32.bor) combines the bits of both inputs together and returns it as an unsigned number.

Example:

local OR = bit32.bor(14,17)
-- The 'OR' Variable would equal to 31, because:

-- 0000 1110 (14)
--- 0001 0001 (17)

-- if 1 or both of the values are 1, the output is 1. 

XOR

The XOR operator (bit32.bxor) is similar to OR, but if both bit values are 1, the output is 0, else if one of the bits are 1, then the output is one, if both bits are off, the output is off (0).

Example:

local XOR = bit32.bxor(12,9)
-- The 'XOR' Variable would be equal to 5, because:

-- 0000 1100(12)
-- 0000 1001 (9) 

-- the output 0000 0101 (5) is because bit 3 are both 1, which goes to 0.

NOT

The NOT operator (bit32.bnot) simply just inverts all the bits. It pretty much just returns (2^32)-(1+A), in this case A is the input given.

I do not think I need an example for this as it is pretty simple.

LEFT AND RIGHT SHIFT

Left and Right shift operators (bit32.lshift) and (bit32.rshift) simply shift the bits to the right or left.

Parameters:

bit32.lshift(A,B)

A: The integer that you want to shift.
B: How many bits to shift it by (if negative than it would shift the other way. bit32.lshift(2,-1) == bit32.rshift(2,1).

BONUS: Extract and Rotate

Rotate

The right and left rotate operators (bit32.rrotate) and (bit32.lrotate) act the same as the shifts, but when a number is shifted out of the sequence, it goes to the other end (bit 31).

Extract

bit32.extract has 3 parameters.

A: The number you want to extract the bts from.
B: The starting bit that you want to extract from.
C: How many bits from B to extract.

Example:

local E = bit32.extract(21984,0,8)

-- The 'E' Variable equals to: 224, because:

-- The bits for 21984 are:  00000000000000000101010111100000

-- The extract function started at bit 0, and did the next 8 bits (including bit 0)
-- So it just takes in 11100000, which is 224. 

3 - Usage Examples

1. BitPacking

This would just be a simplified explanation of what BitPacking & BitMasking is. I strongly recommend this tutorial about it.

Packing process

Lets say you want to store 3 or 4 bytes of data into 1 number, such as for RGBs.

first of all, get your 3 or 4 integers between 0 and 255

local A = 254
local B = 89
local C = 13
local D = 12

If saving RGB, eithier leave D as blank or use it to hold a different value between 0 and 255.

Then We would shift B, C and D by 8 bits apart from each other.

	local ShiftA = A
	local ShiftB = bit32.lshift(B,8)
	local ShiftC = bit32.lshift(C,16)
	local ShiftD = bit32.lshift(D,24)

And then you would need to combine all these values into 1 number with:

local Total = bit32.bor(ShiftA,ShiftB,ShiftC,ShiftD)

Entire script:

local A = 254
local B = 89
local C = 13
local D = 12

local ShiftA = A
local ShiftB = bit32.lshift(B,8)
local ShiftC = bit32.lshift(C,16)
local ShiftD = bit32.lshift(D,24)

local Total = bit32.bor(ShiftA,ShiftB,ShiftC,ShiftD)

Unpacking process

The unpacking process is simple. We just extract the bits back into their original form.

local A = bit32.extract(Total,0,8)
local B = bit32.extract(Total,8,8)
local C = bit32.extract(Total,16,8)
local D = bit32.extract(Total,24,8)

A, B, C and D would be the same value from before they were packed.

2. String Inverser

This function inverses a string’s binary contents, acting as a very basic form of encription.

Output Example:
'Make sure to like the post!����ߌ���ߋ�ߓ���ߋ��ߏ����

����ߌ���ߋ�ߓ���ߋ��ߏ����@B@B@B@B s@B@B@B t@B l@B@B@B t@B@B p@B@B@B@B

local function InverseString(String:string)
	local Sequence = ''


	for c = 1, #String do
		local character = string.sub(String,c)
		Sequence..=string.char(bit32.extract(bit32.bnot(string.byte(character)),0,8))
	end

	return Sequence
end

That’s it!

That is the tutorial! Remember to like if you found this helpful and reply if you are confused or got suggestions I should add.

Try out the SimpleBit Module that has some of these functions and much more!

51 Likes

if your working with binary in lua you can use the prefix 0b to denote a binary number like so

local mybinary = 0b00010100
print(mybinary) -- prints 20
8 Likes

Thanks for notifying me about that.

I recommend using that over tonumber(_,2) as tonumber has a limit of 32 bits, which makes it hard to represent binary over the 32bits or (2^32)-1

1 Like

This is literally amazing :no_mouth:… nice job!

It’s not possible to represent all 64-bit integers in Luau regardless. Luau numbers can only represent all 32-bit integers. That’s because Luau uses float64 (otherwise known as binary64, or double) for numbers, and since floats have to fit in non-integer values as well, they sacrifice some of their ability to store integers.

I realised that I do not provide enough information about rrotate and lrotate, so here is a more detailed description:

Lets say we got 0000 0100 as our number.

When we rrotate it, all the bits are shifted to the left.
Lets repeat the process until bit 0 is active.

0000 0010
0000 0001

Now that our first bit (bit 0) is active. If we lrotate it again. bit 0 would be moved to bit 7.

1000 0000

Our number turned from 1 to 2^7 in one rotate.

Of course, this is the Bit32 libary, and instead of working with a byte (8 bits) we work with 32 bits.

So when we shift Bit 0, if it was active then bit 31 would be active.

Bit 31’s value is 2^31, so it would make the number a lot larger.

All even numbers have bit 0 as inactive/false/0
All odd numbers have bit 0 as active/true/1

So the value would change significantly if the number is odd, but not as much if it was even.

lrotate does the same thing but in the other way.

Once bit31 is lrotated, it would have it’s value moved to bit 0.

The 2nd arguement for the rotate commands are how many bits you want to rotate it by.

rrotate(1,-1) would be same as lrotate(1,1), and via spersa.

you can do rrotate(1,20) or with lrotate, or with any integer.

But if the number given is over 31 or under -31, it would shift the integers by (arg-31) (0)

I hope this clarified some misconceptions!

3 Likes

I just wanna say, im reading on the studio now and im seeing all these numbers BROOOO Im so so so confused rn

Not sure if this will ever get used in Roblox. Great concept and explanation! Effort post :DD

1 Like

So how does everything come together when u are working with negative numbers?
Im trying to convert a vector3int16 into bit32 and as a test the positions are completely random, that also means that in some cases it can be negative and that causes the script to misinterpret the number when it decodes it. For example -5 turns into 16379

Bit32 libary does not, and as I’m aware, is not planning to add negative number support.

A way to simulate negative numbers is to shift each bit’s value so that bit 1’s value is 1 and bit 0’s value is 1/2, and so on making bit 0’s value 1/4, bit 1’s value 1/2 and vit 2’s value of 1.

Instead of the calculation 2^bitindex*value (value being either 1 or 0), an offset would need to be added.

Example:

2^(bitindex-offset)*value

Arithmetic with bit32 will not be impacted if both bits have same offset values.

With the original thread soon being 2 years old, I’ll make a more in-depth tutorial on the Bit32 library and on Binary Multiplication, Division and other calculations. I’ll try release the updated tutorial right on the 2 year anniversary of this post.

New, updated tutorial comming soon as it’s the two-year anniversary of the original post!

3 Likes