Mapping primitive, structure, array, NIO buffer, class and object types with Java Native Access (JNA)
Repository
https://github.com/java-native-access/jna
Overview
In this tutorial we will learn primitive, structure, array, NIO buffer, class and object types mapping between Java and C++ using Java Native Access(JNA) library.
For more information on Java Native Access(JNA) library please see previous tutorials series
- Calling android C/C++ code with Java Native Access (JNA)
- Building android shared library and calling with Java Native Access (JNA)
- Building and using multiple android shared libraries
- Handling C++ callbacks, Logging and exceptions with Java Native Access (JNA)
Requirements
- Android Studio 3.0 or higher
- Android NDK
Tutorial covers
- Creating Android Studio project
- Configuring JNA AAR library
- Loading C++ shared library in Java
- String type mapping
- Integer type mapping
- Decimal type mapping
- Boolean type mapping
- Class type mapping
- Object type mapping
- Structure type mapping
- Array type mapping
- NIO buffer type mapping
Difficulty
- Intermediate
Guide
1. Creating Android Studio project
Create new android project and change Application name you can also change Company domain and Package name according to requirements, select Include C++ support and click next
Select minimum SDK version and click next
Select Empty Activity and click next
Change Activity Name and Layout Name according to requirements and click Next
Select C++ Standard as Toolchain Default and click Finish to create project
If you get NDK not configured error in Messages window then click on File menu and select Project Structure and set Android NDK location.
2. Configuring JNA AAR library
Download jna.aar and create New Module and select Import JAR/AAR Package and click Next
Select jna.aar file from file system and click Finish
jna module added to project, open build.gradle file and add jna module under dependencies
dependencies {
implementation project(':jna')
....
....
}
3. Loading C++ shared library in Java
static {
Native.register(MainActivity.class, "native-lib");
}
In static block of a class we load our shared library, first argument to Native.register() method is the class in which we defined our native methods and 2nd argument is the name of native shared library. Name of shared library is native-lib we can change this default name in CMakeLists.txt file which is available in app folder. Loading should be done in static block which will load shared library during class loading time.
Note: C++ methods we want to export are marked with extern "C" to avoid name mangling.
4. String type mapping
In this example we will send string from java to C++ and log it in C++. Note if you are not familiar with logging in C++ please read previous tutorial T4 in this series.
This example contains simple C++ method that accept one string argument and logs it. Here is complete C++ code
#include <jni.h>
#include <android/log.h>
extern "C"
void stringMapping(jstring msg) {
__android_log_print(ANDROID_LOG_DEBUG , "System.out", "%s", msg);
}
Mapping this C++ method in java is similar, we need to add native keyword which tells compiler that method is implemented in native code
public native void stringMapping(String msg);
Calling native methods in java is same as calling other java methods here is Java code for calling stringMapping()
stringMapping("Hello");
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Native.register(MainActivity.class, "native-lib");
}
public native void stringMapping(String msg);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
stringMapping("Hello");
}
}
Output
Output of C++ log statement is in logcat window
5. Integer type mapping
By value
In this example we will pass integer types from Java to C++ by value, which means copies of original values will be passed to C++ and any changes to those copies will not change the original ones
#include <jni.h>
#include <android/log.h>
extern "C"
void integerMappingByValue(jshort a, jint b, jlong c) {
__android_log_print(ANDROID_LOG_DEBUG ,
"System.out", "Before: %hu, %d, %ld", a, b, c);
a = (jshort) (a + 1);
b = (jint) (b + 1);
c = (jlong) (c + 1);
}
Mapping this C++ method in java is similar, we need to add native keyword which tells compiler that method is implemented in native code
public native void integerMappingByValue(short a, int b, long c);
Calling native method in java is same as calling other java methods, for calling native method we will pass three integer types to native method and after native call we are printing those values to see changes made in native method affect original values or not
short a1 = 10; int b1 = 20; long c1 = 30L;
integerMappingByValue(a1, b1, c1);
println("After: " + a1 + ", " + b1 + ", " + c1);
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Native.register(MainActivity.class, "native-lib");
}
public native void integerMappingByValue(short a, int b, long c);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
short a1 = 10; int b1 = 20; long c1 = 30L;
integerMappingByValue(a1, b1, c1);
System.out.println("After: " + a1 + ", " + b1 + ", " + c1);
}
}
Output
Output of log statements is in logcat window, since we are sending integers by value, original values didn't change
By reference
In this example we will pass integer types from Java to C++ by reference, which means references of original values will be passed to C++ and any changes to those references will also change the original ones. For reference type we will use pointers in C++
extern "C"
void integerMappingByRef(jshort *a, jint *b, jlong *c) {
__android_log_print(ANDROID_LOG_DEBUG ,
"System.out", "Before: %hu, %d, %ld", *a, *b, *c);
*a = (jshort) (*a + 1);
*b = (jint) (*b + 1);
*c = (jlong) (*c + 1);
}
To map this C++ method in java we need to add native keyword which tells compiler that method is implemented in native code. Since we will pass values by references so we used Integer references types in method arguments
public native void integerMappingByRef(ShortByReference a, IntByReference b, LongByReference c);
Calling native methods in java is same as calling other java methods we created integer reference types and passed to native C++ method and then after native method call we logs the values to see those original values got changed or not
ShortByReference a = new ShortByReference((short) 10);
IntByReference b = new IntByReference(20);
LongByReference c = new LongByReference(30L);
integerMappingByRef(a, b, c);
println("After: " + a.getValue() + ", " + b.getValue() + ", " + c.getValue());
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Native.register(MainActivity.class, "native-lib");
}
public native void integerMappingByRef(ShortByReference a, IntByReference b, LongByReference c);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ShortByReference a = new ShortByReference((short) 10);
IntByReference b = new IntByReference(20);
LongByReference c = new LongByReference(30L);
integerMappingByRef(a, b, c);
System.out.println("After: " + a.getValue() + ", " + b.getValue() + ", " + c.getValue());
}
}
Output
Output of log statements is in logcat window, since we are passing integers by references, original values also got changed
6. Decimal type mapping
By value
In this example we will pass decimal types from Java to C++ by value, what that means copies of original values will be passed to C++ and any changes to those copies will not change the original ones
#include <jni.h>
#include <android/log.h>
extern "C"
void decimalMappingByValue(jfloat a, jdouble b) {
__android_log_print(ANDROID_LOG_DEBUG ,
"System.out", "Before: %ff, %fd", a, b);
a = (jfloat) (a + 1);
b = (jdouble) (b + 1);
}
To map this C++ method in java we need to add native keyword which tells compiler that method is implemented in native code
public native void decimalMappingByValue(float a, double b);
Calling native method in java is same as calling other java methods, for calling native method we will pass three decimal types to native method and after native call we are printing those values to see changes made in native method affect original values or not
float a = 20.0f;
double b = 30.0d;
decimalMappingByValue(a, b);
System.out.println("After: " + a + "f, " + b + "d");
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Native.register(MainActivity.class, "native-lib");
}
public native void decimalMappingByValue(float a, double b);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
float a = 20.0f;
double b = 30.0d;
decimalMappingByValue(a, b);
System.out.println("After: " + a + "f, " + b + "d");
}
}
Output
Output of log statements is in logcat window, since we are sending decimals by value, original values didn't change
By reference
In this example we will pass decimal types from Java to C++ by reference, what that means references of original values will be passed to C++ and any changes to those references will also change the original ones. For reference type we will use pointers in C++
#include <jni.h>
#include <android/log.h>
extern "C"
void decimalMappingByRef(jfloat *a, jdouble *b) {
__android_log_print(ANDROID_LOG_DEBUG ,
"System.out", "Before: %ff, %fd", *a, *b);
*a = (jfloat) (*a + 1);
*b = (jdouble) (*b + 1);
}
To map C++ method in Java we need to add native keyword which tells compiler that method is implemented in native code. Since we will pass values by references so we used decimal references types in method arguments
public native void decimalMappingByRef(FloatByReference a, DoubleByReference b);
Calling native methods in java is same as calling other java methods we created decimal reference types and passed to native C++ method and then after native method call we logs the values to see those original values got changed or not
FloatByReference a = new FloatByReference(20.0f);
DoubleByReference b = new DoubleByReference(30.0d);
decimalMappingByRef(a, b);
println("After: " + a.getValue() + "f, " + a.getValue() + "d");
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Native.register(MainActivity.class, "native-lib");
}
public native void decimalMappingByRef(FloatByReference a, DoubleByReference b);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FloatByReference a = new FloatByReference(20.0f);
DoubleByReference b = new DoubleByReference(30.0d);
decimalMappingByRef(a, b);
System.out.println("After: " + a.getValue() + "f, " + a.getValue() + "d");
}
}
Output
Output of log statements is in logcat window, since we are passing decimals by references, original values also got changed
7. Boolean type mapping
By value
In this example we will pass boolean type from Java to C++ by value, what that means copy of original value will be passed to C++ and any changes to this copy will not change the original one
#include <jni.h>
#include <android/log.h>
extern "C"
void booleanMappingByValue(jboolean a) {
__android_log_print(ANDROID_LOG_DEBUG,
"System.out",
"Before: a: %s", a ? "true" : "false");
a = (jboolean) !a;
}
Mapping this C++ method in java is similar, we need to add native keyword which tells compiler that method is implemented in native code
public native void booleanMappingByValue(boolean a);
Calling native method in java is same as calling other java methods, for calling native method we will pass boolean type to native method and after native call we are printing this boolean value to see changes made in native method affect original value or not
boolean a = false;
booleanMappingByValue(a);
System.out.println("After: a:" + a);
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Native.register(MainActivity.class, "native-lib");
}
public native void booleanMappingByValue(boolean a);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
boolean a = false;
booleanMappingByValue(a);
System.out.println("After: a:" + a);
}
}
Output
Output of log statements is in logcat window, since we are sending boolean by value, original boolean value didn't change
By reference
In this example we will pass boolean type from Java to C++ by reference, what that means reference of original boolean value will be passed to C++ and any changes to this boolean references will also change the original one. For reference type we will use pointers in C++
#include <jni.h>
#include <android/log.h>
extern "C"
void booleanMappingByRef(jboolean *a) {
__android_log_print(ANDROID_LOG_DEBUG,
"System.out",
"Before: a: %s", *a ? "true" : "false");
*a = (jboolean) !*a;
}
We need to add native keyword which tells compiler that method is implemented in native code. There is no boolean reference type in Java, we will use IntByReference() as a replacement for boolean. integer 0 means false and 1 means true
public native void booleanMappingByRef(IntByReference a);
Calling native methods in java is same as calling other java methods we created integer reference type and passed to native C++ method and then after native method call we logs the values to see those original values got changed or not
IntByReference a = new IntByReference(0);
booleanMappingByRef(a);
System.out.println("After: a: " + (a.getValue() == 1 ? "true" : "false"));
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Native.register(MainActivity.class, "native-lib");
}
public native void booleanMappingByRef(IntByReference a);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
IntByReference a = new IntByReference(0);
booleanMappingByRef(a);
System.out.println("After: a: " + (a.getValue()==1 ? "true" : "false"));
}
}
Output
Output of log statements is in logcat window, since we are passing integer by reference, original integer reference value also got changed
8. Class type mapping
In this example we will pass class type from Java to C++. Having class type in C++ we will call static method and access static property of that class type. We will pass two arguments to C++ one is pointer to JNIEnv, using this pointer we can access java types, properties, creates new types etc, other argument is class type
#include <jni.h>
#include <android/log.h>
extern "C"
void classMapping(JNIEnv *env, jclass clazz) {
//get static field of java class and prints it
jfieldID fieldID = env->GetStaticFieldID(clazz, "name", "Ljava/lang/String;");
jstring nameString = (jstring) env->GetStaticObjectField(clazz, fieldID);
__android_log_print(ANDROID_LOG_DEBUG,
"System.out",
"%s", env->GetStringUTFChars(nameString, 0));
//call to static method of java class
jmethodID updateMethod = env->GetStaticMethodID(clazz, "update", "(I)V");
env->CallStaticVoidMethod(clazz, updateMethod, 100);
}
To map this C++ method in java we need to add native keyword which tells compiler that method is implemented in native code. We will pass JNIEnv along with class type
public native int classMapping(JNIEnv env, Class<?> clazz);
Calling native method in java is same as calling other java methods, for calling native method we will pass JNIEnv and MainActivity class. MainActivity class contains static property and static method which we will call from C++
classMapping(JNIEnv.CURRENT, MainActivity.class);
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Map<String, Boolean> options = Collections.singletonMap(Library.OPTION_ALLOW_OBJECTS, Boolean.TRUE);
Native.register(MainActivity.class, NativeLibrary.getInstance("native-lib", options));
}
public static String name = "FaoB";
public static void update(int res) {
System.out.println("update " + res);
}
public native void classMapping(JNIEnv env, Class<?> clazz);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
classMapping(JNIEnv.CURRENT, MainActivity.class);
}
}
Output
Output of log statements is in logcat window, Logs shows printing of java static property in C++ and call to java static method from C++
9. Object type mapping
In this example we will pass object type from Java to C++. Having object type in C++ we will call non static method and access non static property of that object type. We will pass two arguments to C++ one is pointer to JNIEnv, using this pointer we can access java types, properties, creates new types etc, other argument is object type
#include <jni.h>
#include <android/log.h>
extern "C"
void objectMapping(JNIEnv *env, jobject obj) {
//get non static field of java object and prints it
jclass clazz = env->GetObjectClass(obj);
jfieldID fieldID = env->GetFieldID(clazz, "phone", "Ljava/lang/String;");
jstring phoneString = (jstring) env->GetObjectField(obj, fieldID);
__android_log_print(ANDROID_LOG_DEBUG,
"System.out",
"%s", env->GetStringUTFChars(phoneString, 0));
//call to non static method of java object
jmethodID method = env->GetMethodID(clazz, "update", "(I)V");
env->CallVoidMethod(obj, method, 200);
}
To map this C++ method in java we need to add native keyword which tells compiler that method is implemented in native code. We will pass JNIEnv along with object type
public native void objectMapping(JNIEnv env, MainActivity object);
Calling native method in java is same as calling other java methods, for calling native method we will pass JNIEnv and MainActivity object reference. MainActivity class contains non static property and non static method which we will call from C++
objectMapping(JNIEnv.CURRENT, this);
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Map<String, Boolean> options = Collections.singletonMap(Library.OPTION_ALLOW_OBJECTS, Boolean.TRUE);
Native.register(MainActivity.class, NativeLibrary.getInstance("native-lib", options));
}
public String phone = "12345678";
public void update(int res) {
System.out.println("update " + res);
}
public native int objectMapping(JNIEnv env, MainActivity object);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
objectMapping(JNIEnv.CURRENT, this);
}
}
Output
Output of log statements is in logcat window, Logs shows printing of java non static property in C++ and call to java non static method from C++
10. Structure type mapping
By value
In this example we will pass structure type from Java to C++ by value, which means if we make changes to structure in C++ it will will not change structure value on java side. In this example we create simple structure with double value, we are passing structure as method argument. In method body we are making changes to structure value
#include <android/log.h>
struct TestStructure {
double value;
};
extern "C"
void structureByValue(TestStructure str) {
__android_log_print(ANDROID_LOG_DEBUG,
"System.out",
"Before: value = %f", str.value);
str.value = 23.34;
}
To map C++ structure in Java we need to extend from Structure class and we can add variables in our case we added double value to match C++ type. We need to create inner static class ByValue to pass structure by value
class TestStructure extends Structure {
double value;
static class ByValue extends TestStructure implements Structure.ByValue{}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("value");
}
}
To map this C++ method in java we need to add native keyword which tells compiler that method is implemented in native code. We will pass structure to native method
public native void structureByValue(TestStructure.ByValue str);
We first created structure by value and set its double value, then we pass that structure to native method and then prints the log statement to see its value got changed or not
TestStructure.ByValue struct = new TestStructure.ByValue();
struct.value = 55.6;
structureByValue(struct);
System.out.println("After: value = " + struct.value);
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Native.register(MainActivity.class, "native-lib");
}
public native void structureByValue(TestStructure.ByValue str);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TestStructure.ByValue struct = new TestStructure.ByValue();
struct.value = 55.6;
structureByValue(struct);
System.out.println("After: value = " + struct.value);
}
}
class TestStructure extends Structure {
double value;
static class ByValue extends TestStructure implements Structure.ByValue {}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("value");
}
}
Output
Output of log statements is in logcat window, Logs shows original structure contents on java side didn't change when we make changes in C++
By reference
In this example we will pass structure type from Java to C++ by reference, which means if we make changes to structure in C++ it will also changes structure value on java side. In this example we create simple structure with double value, we are passing pointer to that structure as method argument. In method body we are making changes to structure value
#include <android/log.h>
struct TestStructure {
double value;
};
extern "C"
void structureByRef(TestStructure *str) {
__android_log_print(ANDROID_LOG_DEBUG,
"System.out",
"Before: value = %f", str->value);
str->value = 23.34;
}
To map C++ structure in Java we need to extend from Structure class and we can add variables in our case we added double value to match C++ type. We need to create inner static class ByReference to pass structure by reference
class TestStructure extends Structure {
double value;
static class ByReference extends TestStructure implements Structure.ByReference {}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("value");
}
}
To map this C++ method in java we need to add native keyword which tells compiler that method is implemented in native code. We will pass structure to native method
public native void structureByRef(TestStructure.ByReference str);
We first created structure by reference and set its double value, then we pass that structure to native method and then prints the log statement to see its double value got changed or not
TestStructure.ByReference struct = new TestStructure.ByReference();
struct.value = 55.6;
structureByRef(struct);
System.out.println("After: value = " + struct.value);
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Native.register(MainActivity.class, "native-lib");
}
public native void structureByRef(TestStructure.ByReference str);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TestStructure.ByReference struct = new TestStructure.ByReference();
struct.value = 55.6;
structureByRef(struct);
System.out.println("After: value = " + struct.value);
}
}
class TestStructure extends Structure {
double value;
static class ByReference extends TestStructure implements Structure.ByReference {}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("value");
}
}
Output
Output of log statements is in logcat window, Logs shows original structure contents on java side also got changed when we make changes in C++
11. Array type mapping
In this example we will pass array type from Java to C++. Arrays can only be passed as reference so changes made in C++ will also changes original array. In this example we are passing pointer to int array and array length as arguments, and in method body we are multiplying each element of array with 2. We can also pass other integer and decimal type arrays in similar manner
extern "C"
void arrayMapping(int *array, int len) {
for (int i = 0; i < len; i++) {
array[i] = array[i] * 2;
}
}
To map this C++ method in java we need to add native keyword which tells compiler that method is implemented in native code. We will pass array along with array length
public native void arrayMapping(int array[], int len);
Calling native method in java is same as calling other java methods, for calling native method we first initializes the array and passed to native method and then prints it to see its contents got changed or not
int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
arrayMapping(array, array.length);
for (int i : array) {
System.out.println(i);
}
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Native.register(MainActivity.class, "native-lib");
}
public native void arrayMapping(int array[], int len);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
arrayMapping(array, array.length);
for (int i : array) {
System.out.println(i);
}
}
}
Output
Output of log statements is in logcat window, Logs shows original array contents also got changed when we make changes in C++
12. NIO buffer type mapping
In this example we will pass NIO buffer type from Java to C++. NIO buffers can only be passed as reference so changes made in C++ will also changes original buffer. In this example we are passing pointer to int buffer and buffer length as arguments, and in method body we are multiplying each element of buffer with 2. We can also pass other integer and decimal NIO buffer types in similar manner
extern "C"
void bufferMapping(int *buffer, int len) {
for (int i = 1; i <= len; i++) {
buffer[i] = buffer[i] * 2;
}
}
To map this C++ method in java we need to add native keyword which tells compiler that method is implemented in native code. We will pass NIO int buffer along with buffer length
public native void bufferMapping(IntBuffer buffer, int len);
Calling native method in java is same as calling other java methods, for calling native method we first initializes the buffer and passed to native method and then prints it to see its contents got changed or not
IntBuffer intBuffer = IntBuffer.allocate(10);
for (int i = 1; i <= 10; i++) {
intBuffer.put(i);
}
intBuffer.flip();
bufferMapping(intBuffer, intBuffer.capacity());
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
Here is complete Java code for this example
public class MainActivity extends AppCompatActivity {
static {
Native.register(MainActivity.class, "native-lib");
}
public native void bufferMapping(IntBuffer buffer, int len);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
IntBuffer intBuffer = IntBuffer.allocate(10);
for (int i = 1; i <= 10; i++) {
intBuffer.put(i);
}
intBuffer.flip();
bufferMapping(intBuffer, intBuffer.capacity());
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
Output
Output of log statements is in logcat window, Logs shows original buffer contents also got changed when we make changes in C++
Github
Complete project available on github. Clone repo and open project name T5.
Congratulations @kabooom! You have completed some achievement on Steemit and have been rewarded with new badge(s) :
Award for the number of posts published
Click on any badge to view your own Board of Honor on SteemitBoard.
To support your work, I also upvoted your post!
For more information about SteemitBoard, click here
If you no longer want to receive notifications, reply to this comment with the word
STOP
Thank you for your contribution.
While I liked the content of your contribution, I would still like to extend few advices for your upcoming contributions:
Looking forward to your upcoming tutorials.
Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]
Hi thanks for response i added images to show the output of example not the code. the complete code for each example is included above Output section. I will improve my english skills
Hey @kabooom
Thanks for contributing on Utopian.
We're already looking forward to your next contribution!
Contributing on Utopian
Learn how to contribute on our website or by watching this tutorial on Youtube.
Want to chat? Join us on Discord https://discord.gg/h52nFrV.
Vote for Utopian Witness!