前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++11 JNI开发中RAII的应用(一)--制作基础工具

C++11 JNI开发中RAII的应用(一)--制作基础工具

作者头像
10km
发布2022-05-07 10:10:03
3700
发布2022-05-07 10:10:03
举报
文章被收录于专栏:10km的专栏

背景

最近项目C++底层代码写完了,开始做java与底层代码的接口部分,就涉及到JNI编程,我这是第一次写JNI代码,看了很多资料,得到一个印象:JNI开发本身不复杂,但如果操作不慎,很容易造成内存泄露参见《jni 内存泄露》,而且最容易被忽视的就是本地引用(LocalReference)造成的内存泄露。 按照官方对的文档 《Java Native Interface Specification Contents》的描述,

Global and Local References The JNI divides object references used by the native code into two categories: local and global references. Local references are valid for the duration of a native method call, and are automatically freed after the native method returns. Global references remain valid until they are explicitly freed.

上面这段话的大意是Local Reference 在 native method 执行完成后,会自动被释放,而全局引用 需要显式释放,。如果你只看了这段话就认为,全局引用需要显式释放(explicitly freed),而本地引用不需要显示式释放,似乎不会造成任何的内存泄漏,那就错了。

再看紧接着的这段描述:

In most cases, the programmer should rely on the VM to free all local references after the native method returns. However, there are times when the programmer should explicitly free a local reference. Consider, for example, the following situations: A native method accesses a large Java object, thereby creating a local reference to the Java object. The native method then performs additional computation before returning to the caller. The local reference to the large Java object will prevent the object from being garbage collected, even if the object is no longer used in the remainder of the computation. A native method creates a large number of local references, although not all of them are used at the same time. Since the VM needs a certain amount of space to keep track of a local reference, creating too many local references may cause the system to run out of memory. For example, a native method loops through a large array of objects, retrieves the elements as local references, and operates on one element at each iteration. After each iteration, the programmer no longer needs the local reference to the array element.

上面的这段举出了两个场景来说明对本地引用有时也需要显式地释放。第一个场景说明无用的本地引用会无谓的占用jvm的资源。第二个场景以在jni代码(native code)中创建java 对象数组为例说明,在jni代码中创建大量本地引用而不显式释放可能会导致out of memory。

基本思路–RAII

看到这里我好害怕。。。我的确要在native code中频繁创建对象,创建array of object ,还可能会调用java对象中的方法返回的也是java object,。。。。这些都是local reference,如果每个java object 用完后都要调用DeleteLocalRef来释放,那么代码的复杂度也太高了,瞬间觉得好烦。

烦过之后呢,还得想办法。。。反正我不想在写代码时总掂记着引用有没有释放的问题。 于是我想到了我之前写的RAII类(参见我之前的博客《C++11实现模板化(通用化)RAII机制》),我的基本思路有了:

将每个java对象的local reference用我之前写的raii_var类封装成一个RAII机制管理的对象,就可以实现在作用域结束时自动调用DeleteLocalRef释放的功能了。。。

想到这里我的心里豁然开朗,前几天写的RAII类这下派上大用场啦。

改进raii_var

首先,为方便使用,我对《C++11实现模板化(通用化)RAII机制》中提到的raii_var类进行了改造,增加了*和->操作符:

代码语言:javascript
复制
    // *操作符,返回T对象引用
    T& operator*() noexcept
    { return get();}    
    /* 根据 T类型不同选择不同的->操作符模板 */
    template<typename _T=T>
    typename std::enable_if<std::is_pointer<_T>::value,_T>::type operator->() const noexcept
    { return resource;}//T为指针时直接返回指针本身
    template<typename _T=T>
    typename std::enable_if<std::is_class<_T>::value,_T*>::type operator->() const noexcept
    { return std::addressof(resource);}// T是class/struct时返回resource的地址
//这里->操作符使用了函数模板实现,用到了is_pointer和is_class两个type_trait来判断T的类型,
//如果T不是指针,也不是class/struct,则没有->操作符实现

同样为了方便调用,还增加了支持类型转换的模板函数_get(),允许指定返回类型调用_get(),比如 raii_obj._get<jstring>(),调用_get()时将raii_obj中的对象转成jstring类型

代码语言:javascript
复制
    /* 根据 _T类型不同选择不同的函数模板 */
    template<typename _T=T>
    typename std::enable_if<!std::is_same<_T,T>::value&&std::is_class<_T>::value,_T&>::type _get() noexcept
    { return static_cast<_T&>(resource); }//T和_T不相等且_T是class/struct时直接将resource转换为_T的引用
    template<typename _T=T>
    typename std::enable_if<!std::is_same<_T,T>::value&&std::is_pointer<_T>::value, _T>::type _get() noexcept
    { return static_cast<_T>(resource); }//T和_T不相等且_T是指针时调用static_cast将resource转换为_T.
//这里用到了is_same来判断模板参数类型是否相同。其实这里应该写得更严谨一些,不仅要判断_T是class,还要判断T也是class,而且T和_T是继承关系,暂时这么写了,以后再改。

有了这个raii_var这个类做最底层的基础类,就可以开始为jni开发定制基础工具啦。

raii_bind_var

为了方便raii_var的调用,再增加一个生成raii_var的模板函数做工具函数raii_bind_var

代码语言:javascript
复制
/* raii方式管理F(Args...)函数生产的对象
 * 如果调用时指定T类型,则返回的RAII对象类型为T,否则类型为F(Args...)结果类型
 */
template<typename T=void,typename F, typename... Args,
                typename ACQ_RES_TYPE=typename std::result_of<F(Args...)>::type,
                typename TYPE=typename std::conditional<!std::is_same<T,void>::value&&!std::is_same<T,ACQ_RES_TYPE>::value,T,ACQ_RES_TYPE>::type,
                typename REL=std::function<void(TYPE&)> >
raii_var<TYPE>
raii_bind_var(REL rel,F&& f, Args&&... args){
    return raii_var<TYPE>(
            [&]()->TYPE {return static_cast<TYPE>(std::bind(std::forward<F>(f), std::forward<Args>(args)...)());},
            rel);
}
//变参函数模板,可以根据需要加入任意数目的参数

有了这个函数,就很方便的可以将任意一个函数(类成员函数/普通函数)的返回结果封装成一个raii_var对象。

比如JNIEnv中的GetObjectClass,如下的代码,就将GetObjectClass的返回结果(jclass)封装在var_class变量中。当{}作用域结束,var_class的析构函数会调用DeleteLocalRefjclass对象的本地引用释放。

代码语言:javascript
复制
JNIEnv* env;
jobject obj;
{
auto var_class=raii_bind_var(
    [](jclass& clazz){env->DeleteLocalRef(clazz);};
    JNIEnv::GetObjectClass,env,obj);
jclass clazz=*var_class;//获取GetObjectClass的结果。
// do something
}

但是如果每个JNIEnv的函数都要这么写也是挺烦的一件事儿。。。怎么办?还要针对jni开发把上面这段代码封装。

丢掉JNIEnv*拖油瓶

当函数封装越来越多层的时候,总是带着JNIEnv*还真是麻烦,所以在做这件事儿之前,我们要先把调用JNI函数时必须用到的JNIEnv*这个拖油瓶给处理掉。

JNIEnv是个跟线程相关的对象,对于每个线程是一样的,每个java naitive method都会提供一个JNIEnv*指针,指向前当线程的JNIEnv对象。但是我们 也可以通过JVM*来获取当前线程的JNIEnv

所以可以设置一个thread_local 类型的静态全局变量以保存当前线程的JNIEnv指针,再提供一个getJNIEnv函数用于获取当前线程的JNIEnv

代码语言:javascript
复制
class jni_utilits {
private:
    static thread_local JNIEnv* const env;
    static void _getJNIEnv(){
        assert(nullptr != JVM);
        //调用JVM::GetEnv获取当前线程的JNIEnv指针
        if (JVM->GetEnv((void **) &const_cast<JNIEnv*&>(env), JNI_VERSION_1_6) != JNI_OK) {
            throw std::logic_error("error for GetEnv");
        }
        assert(nullptr != env);
    }
public:
    static JavaVM * const JVM;
    static JNIEnv* getJNIEnv() {
        if (nullptr == env) {
            _getJNIEnv();//指针为nullptr时调用_getEnv()获取当前线程的指针,每个线程在执行期间只调用一次
        }
        return env;
    }
}

上面的代码中有JNIEnv是通过JVM::getEnv函数获取的,关于如何获取JVM,参见官网关于JNI的相关文档:JNJI_OnLoad和GetEnv。

好了,有了jni_utilits::getJNIEnv()这个神器,我们就彻底甩掉了JNIEnv*这个拖油瓶。我们可以继续我们的封装大业了。

raii_jobject_var

如果像前面例子的代码一样直接调用raii_bind_var,也是挺麻烦的,为了进一步让raii_bind_var更适合JNI开发,应该对raii_bind_var再做一层封装,于是就有了基于raii_bind_var的针对jobjectraii_jobject_var函数。 在函数中加入了对java异常处理的判断,并且已经加入了释放本地引用的代码(DeleteLocalRef):

代码语言:javascript
复制
    /* raii方式管理F函数生产的jobject对象局部引用
     * 如果调用时指定T类型,则返回的RAII对象类型为T,否则类型为F(Args...)结果类型
     */
    template<typename T=void,typename F, typename... Args,
            typename ACQ_RES_TYPE=typename std::result_of<F(Args...)>::type,
            typename TYPE=typename std::conditional<!std::is_same<T,void>::value&&!std::is_same<T,ACQ_RES_TYPE>::value,T,ACQ_RES_TYPE>::type>
    static raii_var<TYPE>
    raii_jobject_var(F&& f, Args&&... args){
        static_assert(
                std::is_base_of<typename std::remove_pointer<ACQ_RES_TYPE>::type,
                                        typename std::remove_pointer<TYPE>::type>::value,
                "T is not derived from jobject");
        auto var= raii_bind_var<TYPE>(
                //析构时释放本地引用
                [](TYPE &obj) {if(nullptr!=obj)getJNIEnv()->DeleteLocalRef(obj);},              
                std::forward<F>(f), std::forward<Args>(args)...
                );
        auto env=getJNIEnv();
        auto exec=env->ExceptionOccurred();//判断是否有java异常
        if(exec){
            //env->ExceptionClear();
            env->ExceptionDescribe();
            //env->Throw(exec);
        }
        return std::move(var);
    }

raii_jobject_env

但是上面的raii_jobject_var函数对于JNIEnv的函数还是需要提供JNIEnv*这个参数,所以针对JNIEnv的方法,再封装一个更简便的调用raii_jobject_env,用这个方法调用JNIEnv的函数,就不用再提供JNIEnv*参数了。

代码语言:javascript
复制
    template<typename T=void,typename F, typename... Args>
    static auto
    raii_jobject_env(F&& f, Args&&... args)->decltype(raii_jobject_var<T>(f,getJNIEnv(),std::forward<Args>(args)...)){
        return raii_jobject_var<T>(f,getJNIEnv(),std::forward<Args>(args)...);
    }
//在调用raii_jobject_var时通过前面所述的jni_utilits::getJNIEnv()函数获取当前线程的JNIEnv指针

好了,到这一步,针对JNI开发的一些基础工具函数已经搭架完了。下面的工作就是要利用这些基础工具,来对JNI开发中所需要的一些函数进行RAII封装了。 再以前面的GetObjectClass为例,就是酱紫滴:

代码语言:javascript
复制
jobject obj;
{
auto var_class=raii_jobject_env(&JNIEnv::GetObjectClass, obj);
jclass clazz=*var_class;//获取GetObjectClass的结果。
// do something
}

更进一步,我们就可以把上面的代码封装成一个更方便的模板函数raii_GetObjectClass

代码语言:javascript
复制
    static raii_var<jclass> raii_GetObjectClass(jobject obj) {
        assert(nullptr != obj);
        return raii_jobject_env(&JNIEnv::GetObjectClass, obj);
    }

调用JNIEnv::GetObjectClass就变成了如此简单的一步。

代码语言:javascript
复制
jobject obj;
{
auto var_class=raii_GetObjectClass(obj);
jclass clazz=*var_class;//获取GetObjectClass的结果。
// do something
}

好了,这其实已经是下一节的内容了,我们在下一节会利用本节所制造的工具把JNI开发的一些基本函数功能统统封装成raii_var管理的对象。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2015-12-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 基本思路–RAII
  • 改进raii_var
  • raii_bind_var
  • 丢掉JNIEnv*拖油瓶
  • raii_jobject_var
  • raii_jobject_env
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档