I'm currently working on (an existing) Android app using native c++ code. I want send a broadcast to another Android Java app listening to a BroadcastReceiver. The receiving app is yet working, but I can't figure out how to broadcast data from JNI code either directly or by sending the values to java code and them broadcasting it from Java.
I hope my explanaion is clear. Can someone help me on how to accomplish this?
Thanks!
The easiest way is probably to do it in Java and write a JNI callback to call that Java function. If you do need to do it in C, you'll need to use JNI to create an Intent, then to set the action and extras of the intent, then use JNI to call the sendBroadcast method of your activity to actually send it. Its a lot of boilerplate JNI code, so I'd try to avoid it.
The easiest way is to implement all logic in a Java method:
public class MyClass {
static {
System.loadLibrary("mylib_jni");
}
public static final String ACTION_TEST = "com.example.action.TEST";
private final LocalBroadCastManager broadcastManager;
public MyClass(Context context) {
broadcastManager = LocalBroadcastManager.getInstance(context);
}
#SuppressWarnings("unused")
private void onSendText(String text) {
final Intent intent = new Intent(ACTION_TEST).putExtra(Intent.EXTRA_TEXT, text);
broadcastManager.sendBroadcast(intent);
}
private native void sendText();
}
and just call that method from JNI:
JNIEXPORT void JNICALL Java_com_example_MyClass_1sendText(JNIEnv *env, jobject instance) {
jclass clazz = (*env)->GetObjectClass(env, instance);
jmethodID method = (*env)->GetMethodID(env, clazz, "onSendText", "(Ljava/lang/String;)V");
jstring text = (*env)->NewString(env, "test message");
(*env)->CallVoidMethod(env, instance, method, text);
}
Related
I am developing a real-time communication app (video and audio) for Android using a WebRTC-based C++ code library. I generate a JNI bridge using SWIG to access the native code from Java. The behaviour of the call is determined via a number of callback functions that are defined in the application layer and passed in a struct to the library code. The function for passing these callbacks looks like this:
void registerCallbacks(CALL_CALLBACKS* callbacks);
where CALL_CALLBACKS is a struct containing a number of callback functions as exemplified here:
struct CALL_CALLBACKS {
// CALL_STATE is an Enum
// Called whenever the state of the call changes
void (*statusCallback)(CALL_STATE);
// FRAME is a struct representing a video frame
// Called whenever the other participant sends a video frame, typically at 30 fps
void (*frameCallback)(const FRAME*);
// ...
}
The issue is that when I let SWIG do its thing by default, the result is unusable. It generates a java type CALL_CALLBACKS which contains setters and getters for each of the callbacks, which are also types generated by SWIG. However, these types are named along the lines of SWIGTYPE_p_f_ENUM_CALL_STATUS__void and are nothing but wrappers for C pointers.
How can I write my SWIG interface file in order to pass callbacks (preferably with less nonsensical names) to the C++ library? I assume typemaps can be used somehow but I just can't wrap my head around how that can be done.
I don't think SWIG can do this by itself. Here's how I would do it in plain JNI:
First, create a Java interface that you can implement on the Java side.
interface Callbacks {
void statusCallback(int status);
void frameCallback(Frame frame);
static native void registerCallbacks(Callbacks cb);
}
Next, create functions that forward the C++ arguments to a jobject g_receiver that implements the interface:
jobject g_receiver;
void my_statusCallback(CALL_STATE s) {
if (!g_receiver) {
// Print a warning?
return;
}
JNIEnv *env = getEnv();
env->PushLocalFrame(10);
jclass cls_Callbacks = env->GetObjectClass(g_receiver);
jmethodID mid_Callbacks_statusCallback = env->GetMethodID(cls_Callbacks, "statusCallback", "(I)V");
env->CallVoidMethod(g_receiver, mid_Callbacks_statusCallback, s);
env->PopLocalFrame(nullptr);
}
void my_frameCallback(const FRAME* frame) {
if (!g_receiver) {
// Print a warning?
return;
}
JNIEnv *env = getEnv();
env->PushLocalFrame(10);
// Create a Frame object from the C++ pointer.
// See Proxy classes at http://swig.org/Doc4.0/Java.html#Java_imclass
jclass cls_Frame = env->FindClass("Frame");
jmethodID ctr_Frame = env->GetMethodID(cls_Frame, "<init>", "(JZ)V");
jobject jFrame = env->NewObject(cls_Frame, ctr_Frame, (jlong) frame, (jboolean)false);
jmethodID mid_Frame_delete = env->GetMethodID(cls_Frame, "delete", "(LFrame;)V");
jclass cls_Callbacks = env->GetObjectClass(g_receiver);
jmethodID mid_Callbacks_frameCallback = env->GetMethodID(cls_Callbacks, "frameCallback", "(LFrame;)V");
env->CallVoidMethod(g_receiver, mid_Callbacks_frameCallback, jFrame);
env->CallVoidMethod(jFrame, mid_Frame_delete); // Disconnect the Java Frame object from the C++ FRAME object.
env->PopLocalFrame(nullptr);
}
CALL_CALLBACKS global_callbacks = { my_statusCallback, my_frameCallback };
Finally, you can implement Callbacks#registerCallbacks as follows. This is a bit more tricky because you have to make sure g_receiver is either nullptr or a valid global reference:
JNIEXPORT void Java_Callbacks_registerCallbacks(JNIEnv *env, jclass cls_Callbacks, jobject receiver) {
if (g_receiver) {
env->DeleteGlobalRef(g_receiver);
}
g_receiver = receiver ? env->NewGlobalRef(receiver) : nullptr;
registerCallbacks(global_callbacks);
}
I made some assumptions:
You did not use any package when generating the code. That will affect the JNI method names and any references to classes in signatures.
I am assuming your native code runs on a different thread, so the getEnv function should use the JNI invocation API to attach the current thread as a daemon thread. You can stash the pointer in another global variable.
Since you're on Android, you can only call FindClass from the main thread. You can work around this by creating a global reference to the class in the JNI_Onload method or hack around it by adding a Class<Frame> getFrameClass() method to the Callbacks interface. Or you can take the very long way round by doing the equivalent of g_receiver.getClass().getClassLoader().findClass("Frame").
You did not specify whether the frameCallback needs to free the FRAME object itself. My code assumes it does not and disconnects the Java object so you cannot accidentally use it after the callback ends.
You did not specify whether the native registerCallbacks could be called multiple times, so I assumed it does. You could also call registerCallbacks as part of JNI_Onload.
I am building an android app and I need to create UI elements dynamically from JNI part and assign C++ functions as click handler.
I have defined a java function to create a button and return it.
I have a C++ function that calls the Java function and has jobject of button. Now I want to assign another C++ function as click handler to this object.
Java:
public class MainActivity extends AppCompatActivity {
...
public View control(String text) {
Button bt = new Button(this);
bt.setText(text);
((ConstraintLayout)findViewById(R.id.layout_main)).addView(bt);
return bt;
}
}
C++:
extern "C" JNIEXPORT void JNICALL Java_com_shaidin_ebellum_MainActivity_start(JNIEnv *env, jobject me)
{
jmethodID jControl = env->GetMethodID(env->GetObjectClass(me), "control", "(Ljava/lang/String;)Landroid/view/View;");
jstring jText = env->NewStringUTF("test");
jobject jView = env->CallObjectMethod(me, jControl, jText);
// How to add listener here?
env->DeleteLocalRef(jView_);
env->DeleteLocalRef(jText);
}
In this question
Android UI in C++
JNIpp library
https://github.com/DmitrySkiba/itoa-jnipp
has introduced and it does exactly what I want, but I want to implement it myself because I don't want to add that much code to my project. Also that library has a complicated implementation using nested macros that makes it difficult to learn how it works.
The process of making a Java object do a callback to C++ code is pretty much always the same; make the Java object store some kind of reference to the native object (typically a pointer), and have it pass that pointer back to the C++ code which can call the appropriate function.
A potential implementation could look like this:
MyOnClickListener.java
public class MyOnClickListener implements View.OnClickListener {
private final long nativePeer;
public MyOnClickListener(final long nativePeer) {
this.nativePeer = nativePeer;
}
#Override
public void onClick(View v) {
OnClick(nativePeer);
}
#Override
public void finalize() {
Release(nativePeer);
}
private static native void OnClick(final long peer);
private static native void Release(final long peer);
}
C++
extern "C" JNIEXPORT void Java_com_example_myapp_MyOnClickListener_OnClick(
JNIEnv *env,
jclass clazz,
jlong nativePeer) {
auto f = reinterpret_cast<std::function<void(void)>*>(nativePeer);
(*f)();
}
extern "C" JNIEXPORT void Java_com_example_myapp_MyOnClickListener_Release(
JNIEnv *env,
jclass clazz,
jlong nativePeer) {
auto f = reinterpret_cast<std::function<void(void)>*>(nativePeer);
delete f;
}
...
// Setting the OnClickListener
// We allocate this object with `new` since we need it to remain alive
// until the Java code no longer needs it. It is the responsibility of
// the Java code to ensure that the memory gets freed.
auto callback = new std::function<void(void)>([] {
__android_log_print(ANDROID_LOG_WARN, "MyOnClickListener", "Hello from native onClick!");
});
jclass listener_clazz = env->FindClass("com/example/myapp/MyOnClickListener");
jmethodID new_listener = env->GetMethodID(listener_clazz, "<init>", "(J)V");
jobject listener = env->NewObject(listener_clazz, new_listener, callback);
jmethodID set_onclicklistener = env->GetMethodID(button_clazz, "setOnClickListener", "(Landroid/view/View$OnClickListener;)V");
env->CallVoidMethod(button, set_onclicklistener, listener);
Note 1: In your real code you should of course add proper error handling and not blindly assume that every JNI call succeeds.
Note 2: Rather than relying on finalize to be called you might want to explicitly invoke the Release function when you no longer need the native peer object.
Note 3: I used a pointer to a std::function in my example. You may want to use a pointer to some class of your own. That's really up to you to decide.
I have a service that can either be started with a launcher or runs on boot. The problem is that when I call a C function with JNI, all the Toast notifications are no longer displayed, whereas if I call only the java method, the Toast notifications display just fine. What causes this and how can I fix it?
Note:
If the C function does something simple like calls back to java and terminates, the Toast notifications will display. My function however, has an infinite loop used to handle interrupts.
ServiceLauncher
public class ServiceLauncher extends AppCompatActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
Toast.makeText(getBaseContext(), "onCreate", Toast.LENGTH_LONG).show();
super.onCreate(savedInstanceState);
startService(new Intent(this, MyService.class));
finish();
}
}
MyService
public class MyService extends Service {
#Override
public IBinder onBind(Intent intent) {
return null;
}
public void onDestroy() {
Toast.makeText(this, "Terminated", Toast.LENGTH_LONG).show();
}
#Override
public int onStartCommand(Intent intent,int flags, int startid) {
Toast.makeText(this, "Running", Toast.LENGTH_LONG).show();
javaMethod(); /*toast displays if this is called*/
cMethodFromJNI(); /*toast does not display if this is called*/
return START_STICKY;
}
}
C code structure
JNIEXPORT jint JNICALL
Java_className_cMethodFromJNI(JNIEnv *env, jclass type) {
jmethodID mid = (*env)->GetStaticMethodID(env, type, "javaMethod", "(I)V");
// open and read some fd (not shown)
for (;;) {
// wait on change in fd and act on it (not shown)
callJavaAgain(env,type,mid)
}
}
//close fd (not shown)
exit(0);
}
EDIT: I've changed the code like so, following the answer below. Seems like the situation is exactly the same as before, perhaps I am missing something.
Revised C code structure
JNIEXPORT jint JNICALL
Java_className_cMethodFromJNI(JNIEnv * env, jclass type) {
/*cache JVM*/
int status = ( * env) - > GetJavaVM(env, & jvm);
if (status != 0) {
LOGD("failed to retrieve *env");
exit(1);
}
attachedThread();
}
void attachedThread() {
/* get a new environment and attach a new thread */
JNIEnv * newEnv;
JavaVMAttachArgs args;
args.version = JNI_VERSION_1_6; // choose your JNI version
args.name = NULL; // if you want to give the java thread a name
args.group = NULL; // you can assign the java thread to a ThreadGroup
( * jvm) - > AttachCurrentThread(jvm, & newEnv, & args);
jclass cls = ( * newEnv) - > FindClass(newEnv, "fully/qualified/class/name");
jmethodID mid = ( * newEnv) - > GetStaticMethodID(newEnv, cls, "callJavaAgain", "(V)V");
// open and read some fd (not shown)
for (;;) {
// wait on change in fd and act on it (not shown)
intermediaryFunction(newEnv, cls, mid);
}
}
//close fd (not shown)
exit(0);
}
void intermediaryFunction(JNIEnv * newEnv, jclass cls, jmethodID mid) {
//do some stuff (not shown)
( * newEnv) - > CallStaticVoidMethod(newEnv, cls, mid);
}
okay so this topic requires some careful attention. Android docs talk about JavaVM and JNIEnv and how to interface with them. What you will need to do is store a global reference to the JavaVM that's accessible from all threads in the C code. The JVM allows a thread to get access to the JNIEnv which can get you access to a class's methods and what not. The next key problem is that you need to stash a jobject 'pointer' to the android object you call back to from the C code (which you probably already know). Make sure you call JNIEnv->DetachThread on the thread that acquired the JNIEnv reference.
so again:
get JavaVM reference and store it in c code
have java call c code to store the jobject reference for your callback
start a thread to do looping, acquire a reference to a JNIEnv on that thread to call to java
call DetachThread before terminating thread execution
another point of trouble is accessing c++ objects from java code, there's a neat trick where you can have c++ code create the java objects, and assign into a private long _cppPointer member on the created Java class, then add this object to some collection on a java object you have already pointer to (like an activity or something)
good luck.
edit: i finally recalled another great source of information about JNI work
I'm experiencing a very strange bug, which I'm thinking might be an environment issue, but I'd like to know if anyone else here is experiencing something similar.
First of all, up until now, everything worked perfectly.
I have a project which works with a jni interface.
Here's the call from my java code:
myJniWrapper.load(CallbackActivity.this); // this code is inside a Runnable
Here's the load method:
public void load(Activity activityCalledFrom) {
if (mLoaded == false) {
loadNative(mNativeHandle, activityCalledFrom); // This is being called correctly.
}
}
private native void loadNative(long nativeHandle, Object activityCalledFrom);
Here's the c++:
JNIEXPORT void JNICALL Java_com_datalogics_dlreader_jni_RMBook_loadNative
(JNIEnv *env, jobject obj, jlong handle, jobject activityCalledFrom)
{
if (handle)
{
jmethodID meth = NULL;
// Convert the long back into a pointer
RMBookNative *theBook = (RMBookNative *) handle;
// Ensure the pointer is valid
if (theBook != NULL)
{
// Get the activity that we were called from
theBook->libraryActivityObj = env->NewGlobalRef(activityCalledFrom);
// Call loadDocument to set the URL for the dpdoc::Document
theBook->loadDocument();
}
}
}
Later on, once the loading is finished, the following code is called in the C++ code:
if (libraryActivityObj != NULL)
{
// Find the loadingFinished method on the library class
meth = jenv->GetMethodID(libraryActivityCls, "loadingFinished", "()V");
// Call the method on the activity
jenv->CallVoidMethod(libraryActivityObj, meth);
}
Then, back in the same activity, I have the following method:
public void loadingFinished()
{
Log.i(getClass().getSimpleName(), "loading finished");
doStuff();
}
The strange thing is, everything is working (at least according to the logs) - and when the c++ tries to call the loadingFinished() method, it instead invokes some random public method in the activity I'm sending - or to be precise, it will call the same random method every time, but if I add another method in the activity, a different method will be called by the tag[:C++] code. It seems to randomly choose a method to call based on the total amount of methods in the activity.
I have tried: cleaning & rebuilding, restarting eclipse, restarting my Mac, restarting the device - nothing helps.
I do not understand what is happening and was hoping if anyone had any insight.
Please let me know if you need me to post more code / general info.
I'm using JNI with Android to use a framework. To access its code I need to access C++ code through JNI, but I'm getting the exception above. I understand the error and the memory access, but I don't know exactly why it's happening. I'm kind a new working with these technologies and after look for some answers, none have helped me.
My .h file:
extern "C"{
JNIEXPORT void JNICALL Java_com_eegeo_MainActivity_AddNewSphere(JNIEnv* jenv, jobject obj, jlong nativeAppWindowPtr, jdouble y);}
My .cpp file, after namespace:
JNIEXPORT void JNICALL Java_com_eegeo_MainActivity_AddNewSphere(JNIEnv* jenv, jobject obj, jlong nativeAppWindowPtr, jdouble y){
Examples::DrawSphereExample* example = (Examples::DrawSphereExample*)(nativeAppWindowPtr);
Eegeo::Space::LatLongAltitude* example2 = (Eegeo::Space::LatLongAltitude*)(nativeAppWindowPtr);
example2->SetAltitude(y);
example->AddSphere((example->getLocation()), Eegeo::v3(1.0f, 1.0f, 0.0f));}
My activity, where it calls that method:
public static native void AddNewSphere(double Yposition);
#Override
public void onClick(View v) {
if(v.getId() == R.id.add_pin){
AddNewSphere(id);
}
}
Debugging the code I notice that is the "example->AddSphere((example->getLocation()), Eegeo::v3(1.0f, 1.0f, 0.0f));" line that it can't execute, giving me this exception. I don't know if it's because the references or other thing.
I appreciate your help.
I figure out that I was trying to do an action dinamically but the framework I'm using does not allow it, that's why I was getting this exception. I've changed my implementation and I don't get this error anymore.