=======================
=======================
=======================
출처: http://www.iamcorean.net/95
작성자 : 김문규
최초 작성일 : 2008. 9. 5
JNI는 Java 클래스에서 C 라이브러리를 호출하는 기술입니다.
구현 방법은 다음과 같습니다.
1. Java 클래스 구현
HelloJniClass.java
public class HelloJniClass {
native void Hello(); // native 함수
static {
System.loadLibrary("Hello_DLL");
} // 로드할 라이브러리 Hello_DLL.dll 을 로드합니다.
public static void main(String args[]) {
HelloJniClass myJNI = new HelloJniClass();
myJNI.Hello(); // native 함수 호출
}
}
> javac HelloJniClass.java
HelloJniClass.class 파일 생성됩니다.
2. C용 헤더 파일 생성
> javah HelloJniClass
HelloJniClass.h 가 생성됩니다. 이 파일은 구현해야 할 함수가 선언되어 있습니다.
클래스패스 선언에 유의합니다. (간단하게는 같은 폴더에 놓고 -classpath . 하면 됩니다.)
3. dll 파일 생성
HelloJniClass.h를 이용해서 원하는 기능을 수행하는 dll 파일을 작성합니다.
HelloWorldLib.cpp
#include "stdafx.h"
#include <stdio.h>
#include "jni.h"
#include "HelloJniClass.h"
JNIEXPORT void JNICALL Java_HelloJniClass_Hello(JNIEnv * env, jobject jobj) {
printf("Hello Jni~!"); // 원하는 기능
}
컴파일 하시면 Hello_DLL.dll 이 생성됩니다.
비줠 스튜디오에서 output 파일 이름을 맞추는 설정을 하시고 프로젝트 include 폴더에 (JDK_HOME)/include/win32를 추가합니다.
4. 실행
> java HelloJniClass
(역시나 클래스패스 설정에 유의하시길 바랍니다.)
Hello Jni~!
5. 완벽한 가이드 문서
=======================
=======================
=======================
jni (c<->jaVA) 연동 |
1. 필요한 파일들.
1.1. NativeStringUtil.c (스트링 변환 함수)
#include <stdlib.h>
#include <jni.h>
#include "NativeStringUtil.h"
/* 효율성을 높이기 위한 캐슁 변수 */
static jclass class_String;
static jmethodID mid_getBytes, mid_getBytesEncoding;
static jmethodID mid_newString, mid_newStringEncoding;
/* C 문자열로부터 자바 바이트 배열을 생성하여 반환
*/
jbyteArray cstr2jbyteArray(JNIEnv * env,
const char * nativeStr) {
jbyteArray javaBytes;
int len = strlen(nativeStr);
javaBytes = ( * env) - > NewByteArray(env, len);
( * env) - > SetByteArrayRegion(
env, javaBytes, 0, len, (jbyte * ) nativeStr);
return javaBytes;
}
/* 자바 바이트 배열로부터 C 문자열을 생성하여 반환
*/
char * jbyteArray2cstr(JNIEnv * env, jbyteArray javaBytes) {
size_t len = ( * env) - > GetArrayLength(env, javaBytes);
jbyte * nativeBytes = ( * env) - > GetByteArrayElements(env, javaBytes, 0);
char * nativeStr = malloc(len + 1);
strncpy(nativeStr, nativeBytes, len);
( * env) - > ReleaseByteArrayElements(env, javaBytes, nativeBytes, JNI_ABORT);
return nativeStr;
}
/* 자바 스트링을 디폴트 인코딩의 자바 바이트 배열로 변환.
* String 클래스의 getBytes() 메쏘드를 호출한다.
*/
jbyteArray javaGetBytes(JNIEnv * env, jstring str) {
if (mid_getBytes == 0) {
if (class_String == 0) {
jclass cls = ( * env) - > FindClass(env, "java/lang/String");
if (cls == 0)
return 0; /* 오류 */
class_String = ( * env) - > NewGlobalRef(env, cls);
if (class_String == 0)
return 0; /* 오류 */
}
mid_getBytes = ( * env) - > GetMethodID(
env, class_String, "getBytes", "()[B");
if (mid_getBytes == 0)
return 0;
}
/* str.getBytes(); */
return ( * env) - > CallObjectMethod(env, str, mid_getBytes);
}
/* 자바 스트링을 지정된 인코딩 `encoding'의 자바 바이트 배열로 변환.
* String 클래스의 getBytes(String encoding) 메쏘드를 호출한다.
*/
jbyteArray javaGetBytesEncoding(JNIEnv * env, jstring str,
const char * encoding) {
if (mid_getBytesEncoding == 0) {
if (class_String == 0) {
jclass cls = ( * env) - > FindClass(env, "java/lang/String");
if (cls == 0)
return 0; /* 오류 */
class_String = ( * env) - > NewGlobalRef(env, cls);
if (class_String == 0)
return 0; /* 오류 */
}
mid_getBytesEncoding = ( * env) - > GetMethodID(env, class_String, "getBytes", "(Ljava/lang/String;)[B");
if (mid_getBytesEncoding == 0)
return 0;
}
/* str.getBytes( encoding ); */
return ( * env) - > CallObjectMethod(env, str, mid_getBytesEncoding, ( * env) - > NewStringUTF(env, encoding));
}
/* 디폴트 인코딩의 자바 바이트 배열을 자바 스트링으로 변환.
* String 클래스의 new String(byte[] bytes) 메쏘드를 호출한다.
*/
jstring javaNewString(JNIEnv * env, jbyteArray javaBytes) {
if (mid_newString == 0) {
if (class_String == 0) {
jclass cls = ( * env) - > FindClass(env, "java/lang/String");
if (cls == 0)
return 0; /* 오류 */
class_String = ( * env) - > NewGlobalRef(env, cls);
if (class_String == 0)
return 0; /* 오류 */
}
mid_newString = ( * env) - > GetMethodID(
env, class_String, "<init>", "([B)V");
if (mid_newString == 0)
return 0;
}
/* new String( javaBytes ); */
return ( * env) - > NewObject(
env, class_String, mid_newString, javaBytes);
}
/* 지정된 인코딩 `encoding'의 자바 바이트 배열을 자바 스트링으로 변환.
* String 클래스의 new String(byte[] bytes, String encoding)
* 메쏘드를 호출한다.
*/
jstring javaNewStringEncoding(
JNIEnv * env, jbyteArray javaBytes,
const char * encoding) {
int len;
jstring str;
if (mid_newString == 0) {
if (class_String == 0) {
jclass cls = ( * env) - > FindClass(env, "java/lang/String");
if (cls == 0)
return 0; /* 오류 */
class_String = ( * env) - > NewGlobalRef(env, cls);
if (class_String == 0)
return 0; /* 오류 */
}
mid_newString = ( * env) - > GetMethodID(
env, class_String, "<init>", "([BLjava/lang/String;)V");
if (mid_newString == 0)
return 0;
}
/* new String( javaBytes, encoding ); */
str = ( * env) - > NewObject(
env, class_String, mid_newString, javaBytes,
( * env) - > NewStringUTF(env, encoding));
return str;
}
1.2. NativeStringUtil.h
#ifndef _Included_NativeStringUtil
#define _Included_NativeStringUtil
#ifdef __cplusplus
extern "C" {
#endif
char *jbyteArray2cstr( JNIEnv *env, jbyteArray javaBytes );
jbyteArray cstr2jbyteArray( JNIEnv *env, const char *nativeStr);
jbyteArray javaGetBytes( JNIEnv *env, jstring str );
jbyteArray javaGetBytesEncoding( JNIEnv *env, jstring str, const char *encoding );
jstring javaNewString( JNIEnv *env, jbyteArray javaBytes );
jstring javaNewStringEncoding(JNIEnv *env, jbyteArray javaBytes, const char *encoding );
#ifdef __cplusplus
}
#endif
#endif
1.3. com_pepsi_user_decode_JDecode.c
- javah -jni 통해 만든 header파일을 재구성해 줌.
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h>
#include <com_pepsi_user_decode_JDecode.h>
#include "NativeStringUtil.h"
#include "Decode.h"
#define BUFSIZE 1024
/*
* Class: JDecode
* Method: decodeString
* Signature: (ILjava/lang/String;Ljava/lang/String;)I
*/
JNIEXPORT jstring JNICALL Java_com_pepsi_user_decode_JDecode_nativeDecodeStr
(JNIEnv * env, jobject jobj, jstring tar) {
char cSource[BUFSIZE];
int iCoopNum = 1;
int rt;
const char * cTarget;
cTarget = jbyteArray2cstr(env, javaGetBytesEncoding(env, tar, "EUC-KR"));
bzero(cSource, sizeof(cSource));
rt = decodeString(cTarget, cSource); // 복호화 함수
( * env) - > ReleaseStringUTFChars(env, tar, cTarget);
return javaNewStringEncoding(env, cstr2jbyteArray(env, cSource), "EUC-KR");
}
1.4.com_pepsi_user_decode_JDecode.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for com_pepsi_user_decode_JDecode */
#ifndef _Included_com_pepsi_user_decode_JDecode
#define _Included_com_pepsi_user_decode_JDecode
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_pepsi_user_decode_JDecode
* Method: nativeDecodeStr
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_pepsi_user_decode_JDecode_nativeDecodeStr
(JNIEnv * , jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
1.5 Decode.h
- 복호화 헤더 파일.
1.6 testCipher.c
- 복호과 구현 파일.
2. so 파일 생성.
- 내용 : 특정 스트링 복호화 하는 라이브러리.
- libDecode.so
2.1 간단한 Makefile
CC=/usr/bin/gcc
CFLAGS=-D_REENTRANT -D_GNU_SOURCE
JNIFLAGS=-I/k2/src/jni/cookie -I/usr/local/j2sdk1.4.2_04/include -I/usr/local/j2sdk1.4.2_04/include/li
nux
OBJS = ./cipher/testCipher.o com_pepsi_user_decode_JDecode.o NativeStringUtil.o
TARGET = libDecode.so
$(TARGET): $(OBJS)
$(CC) -shared -o $@ $(OBJS)
all : $(TARGET)
install :
mv libDecode.so /usr/lib
clean :
\rm -rf *.o *.a *.so
.SUFFIXES: .c .o
.c.o:
$(CC) $(CFLAGS) $(JNIFLAGS) -c -o $@ $<
3. JDecode.java
package com.pepsi.user.decode;
// 패키지 명은 위의 라이브러리
// com_pepsi_user_decode_JDecode.h 와 일치해야
// 일치 안하면 java.lang.UnsatisfiedLinkError 발생한다.
public class JDecode {
static {
System.loadLibrary("Decode"); // load(libDecode.so)도 있다
}
public native String nativeDecodeStr(String psTarget);
}
4. 사용방법
- libDecode.so LD_LIBRARY_PATH혹은 /usr/lib에 카피.
- String decode = new JDecode().nativeDecodeStr(encode);
=======================
=======================
=======================
출처: http://sinuk.egloos.com/2676307
1. JNI (Java Native Interface) 란 ?
- 자바가 다른 언어로 만들어진 어플리케이션과 상호 작용할 수 있는 인터페이스를 제공한다.
- 자바가상머신(JVM)이 원시 메소드(native method)를 적재(locate)하고 수행(invoke)할 수 있도록 한다
- JNI가 자바가상머신내에 포함됨으로써, 자바가상머신이 호스트 운영체제상의 입출력, 그래픽스, 네트워킹, 그리고 스레드와 같은 기능들을 작동하기 위한 로컬시스템호출(local system calls)을 수행할 수 있도록 한다.
* 쉽게 말해 Java와 다른 언어를 연동하는 솔루션입니다.
[그림1] C로 만들어진 Library와 JAVA를 연결해주는 JNI
2. Why do you need JNI ?
자 바 네이티브 메쏘드(Java Native method, 이하 JNI)는 다른 언어로 작성된 코드를 자바에서 호출하도록 만들어진 규약이다. 현재는 C/C++에 대한 호출만을 정확하게 지원한다. 어떻게 보면 JNI는 자바가 만들어진 철학과 정반대되는 것이다.
그러나. Java에도 한계가 있다.
1. 속도 문제가 있는 계산 루틴
> 자바가 Native Code(플랫폼에 종속적인 기계어 코드)에 비해 느리다.
2. 자바에서 하드웨어 제어
3. 자바에서 지원되지 않은 특정 운영체제 서비스
> 자바의 클래스 라이브러리는 방대하고 다양한 서비스를 제공하지만, 특정 플랫폼에서 제공하는 고유의 서비스의 기능을 모두 포함할 수는 없다. 특히, 특수한 목적으로 제작된 하드웨어를 자바에서 제어해야 할 필요가 있다고 한다면, 자바만으로 해결하기는 힘들다.
4. 기존의 프로그램에서 자바가 제공하는 서비스를 이용
> 기존에 작성된 프로그램이나 기존의 시스템(legacy)과의 연계 문제
∴ JNI를 써서 해결해보
3. C를 이용한 JNI 예제
VC++을 이용해 C문법으로 작성되어 만들어진 DLL을 로딩하여 Java에서 사용해보겠습니다.
1단계 : Native Method를 선언하는 자바 클래스 작성
2단계 : 1단계에서 작성한 클래스 컴파일
3단계 : javah를 사용해서 Native Method가 사용할 헤더 파일 생성
4단계 : C언어로 Native Method 실제 구현
5단계 : C 코드와 헤더 파일을 컴파일
6단계 : 자바 프로그램 실행
1단계 : Native Method를 선언하는 자바 클래스 작성
Java 소스 파일 : HelloJni_Jsource.java
import java.util.*;
class HelloJniClass {
native void Hello();
static { System.loadLibrary("Hello_DLL"); }
public static void main(String args[]) {
HelloJniClass myJNI=new HelloJniClass();
myJNI.Hello();
}
}
// 아래는 좀 위의 내용 보충 그림
2단계 : 1단계에서 작성한 클래스 컴파일
* 컴파일시에는 일반 java 컴파일때와 마찬가지로 환경변수 셋팅이 되어 있어야 합니다.
-> Path가 JDK의 Javac.exe가 있는 폴더에 설정되어 있어야 합니다.
3단계 : javah를 사용해서 Native Method가 사용할 헤더 파일 생성
HelloJniClass.h을 열어보면
JNIEXPORT void JNICALL Java_HelloJniClass_Hello (JNIEnv *, jobject);
위의 함수를 Implement만 해서 DLL을 만들면 됩니다. (4단계)
4단계 : C언어로 Native Method 실제 구현(1)
1) VC++ 프로젝트 만들기 : Win32용 DLL 프로젝트로 만듭니다.
New - Projects : Win32 Dynamic-Link Library
2) Add Files Projects : HelloJniClass.h 파일 추가
3) Projects Setings(Alt+F7)
- Link탭에 Output file Name : 1단계의 2. 라이브러리 적재시 작성한 DLL파일명(Hello_DLL.dll)
- C/C++탭 Preprocessor 카테고리의 Additional Include directories
JDK의 Include폴더와 Include폴더 밑의 win32폴더
예) C:\Program Files\Java\jdk1.5.0_03\include\,
C:\Program Files\Java\jdk1.5.0_03\include\win32
4. 값의 전달과 리턴
1단계 : Java 소스 파일 StringPass_Jsource.java
* 일반 자바 메쏘드 선언과 동일합니다.
class JNI_Message {
native byte[] Message(String input);
// 라이브러리 적재(Load the library)
static {
System.loadLibrary("Msg_DLL");
}
public static void main(String args[]) {
byte buf[];
// 클래스 인스턴스 생성(Create class instance)
JNI_Message myJNI=new JNI_Message();
// 원시 메소드에 값을 주고 받음
buf = myJNI.Message("Apple");
System.out.print(buf); // 받은값 출력
}
}
2단계 : 컴파일
javac StringPass_Jsource.java
3단계 : header파일 생성
javah JNI_Message
4단계 : method구현 : StringJNIDLLSource.c
#include <stdio.h>
#include <jni.h>
#include <string.h>
#include "JNI_Message.h"
JNIEXPORT jbyteArray JNICALL Java_JNI_1Message_Message (JNIEnv * env, jobject jobj, jstring input)
{
jbyteArray jb;
jboolean iscopy;
char* buf;
static char outputbuf[20];
buf=(*env)->GetStringUTFChars(env, input, &iscopy); // 입력 String 읽어오는 함수
printf ("\nDLL receive Data from JAVA : %s\n",buf); // 입력받은 내용을 출력
strcpy(outputbuf,"Delicious !!\n");
jb=(*env)->NewStringUTF(env, outputbuf); // 출력할 내용의 java버퍼에 output버퍼값을 셋팅
return(jb); // java버퍼 리턴
}
(*env)->함수명 형태로, JAVA의 메쏘드를 C에서 이용할수 있습니다.
* JAVA는 C로 문자열을 넘겨줄때 UTF-8형태를 사용합니다.
5단계 : 실행
C:\test\C_JNI\Paramerter Pass>java JNI_Message
DLL receive Data from JAVA : Apple
Delicious !!
5. KVM ? KNI ?
KVM은 J2ME의 일부로서 작고 자원이 한정된 기계장치를 위해 설계된 소형 JVM.
JVM에서는 JNI가 KVM의 KNI가 있다.
6. 기타프로그래밍 이슈들
참고 URL :http://www.javastudy.co.kr/docs/jhan/javaadvance/jni.html
언어적 이슈(Language Issues)
메소드 호출(Calling Methods)
필드의 참조(Accessing Fields)
스레드와 동기화(Threads and Synchronization)
메모리 이슈(Memory Issues)
수행(Invocation)
스레드 연결(Attaching Threads)
=======================
=======================
=======================
출처: http://blog.naver.com/PostView.nhn?blogId=nadia407&logNo=80120508458
=======================
=======================
=======================
- AntiStroy 님의 블러그에서 퍼왔음을 알립니다.
프로젝트를 진행하면서 JNI에 대해 알게 되었었다. Java에서 C함수를 호출하거나 C에서 Java의 메소드를 호출할 때 사용하는 것인데, 안드로이드 공부를 하다가 조금 더 파고들게 되었다.
* 안드로이드는 자바로 프로그래밍을 하는데 왜 JNI에 대해 알아야 할까?
우선 안드로이드 플랫폼은 순수하게 Java로만 구성되어진 것이 아닌 Java 레이어와 C/C++레이어가 서로 상호 작용하면서 동작한다. 이 두 레이어가 유기적으로 동작하게 만들려면 JNI에 대해 알아야 한다.
그리고 일반적으로 Java는 C/C++에 비해 느리다. 성능이 중요할 경우 C/C++로 작성하고 이를 JNI를 통해 Java에서 호출할 수 있다.
* NDK(Native Development Kit)
안드로이드 애플리케이션에서 사용할 네이티브 라이브러리를 작성하기 위한 도구 모음.
실제로 모듈을 구현하기 위해서는 JNI에 대한 지식 필요
* JNI의 개발 순서
1. Java 코드 작성
2. Java 코드 컴파일
3. C 헤더 파일 생성
4. C 코드 작성
5. C 공유 라이브러리 생성
6. Java 프로그램 실행
1. Java 코드 작성
public class AtinJNI {
// 네이티브 메소드 선언
native void printAtin();
native void printString(String str);
// 라이브러리 로딩
static {
System.loadLibrary("atinjni");
}
public static void main(String args[]) {
AtinJNI myJNI = new AtinJNI();
// 네이티브 메소드 호출
myJNI.printAtin();
myJNI.printString("Hello Atin");
}
}
[Source 1] AtinJNI.java
[표 1] 플랫폼별 System.loadLibrary() 메소드가 로딩하는 C라이브러리 형식
호스트 플랫폼 | 실제 로드되는 C 라이브러리 |
Window | libname.dll |
Linux | libname.so |
2. Java 코드 컴파일
javac를 통해서 .java파일을 .class로 컴파일한다.
javac AtinJNI.java |
위와 같은 명령을 실행할 경우 아무 에러가 나지 않는다면 .class로 컴파일 된 것이다.
3. C 헤더 파일 생성
이제 Java 소스를 만들었으니 C 소스를 짜야 한다. 그런데 중요한 점은 어떻게 Java에서 호출되는 C 소스를 짤 것인가이다. 이것을 위해 javah라는 툴이 있다. 이 툴을 사용하면 .class파일을 파싱해서 만들어야 할 C 함수의 헤더를 만들어준다.
javah <native로 선언된 메소드를 포함한 자바 클래스명>
- <JDK_HOME>\bin에 포함.
- Java 네이티브 메소드와 연결될 수 있는 C함수의 원형 생성
javah AtinJNI |
위와 같은 명령어를 실행하면 AtinJNI.h라는 헤더 파일이 생성됀다.
내용은 아래와 같다.
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h>
/* Header for class AtinJNI */
#ifndef _Included_AtinJNI
#define _Included_AtinJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: AtinJNI
* Method: printHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_AtinJNI_printHello
(JNIEnv * , jobject);
/*
* Class: AtinJNI
* Method: printString
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_AtinJNI_printString
(JNIEnv * , jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
[Source 2] AtinJNI.h
4. C 코드 작성
위에서 생성한 AtinJNI.h의 헤더를 기준으로 C 소스를 작성하면 됀다.
#include "HelloJNI.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_AtinJNI_printAtin
(JNIEnv * env, jobject obj)
{
printf("Hello Atin \n");
return;
}
JNIEXPORT void JNICALL Java_AtinJNI_printString
(JNIEnv * env, jobject obj, jstring string)
{
const char * str = ( * env) - > GetStringUTFChars(env, string, 0);
printf("%s! \n", str);
return;
}
[Source 3] atinjni.c
5. C 공유 라이브러리 생성
중요한건 Window는 dll, Linux는 so파일을 만들어야 한다. 책에서는 C 소스를 작성 후 cl명령을 통해 dll을 만들었다.
cl 명령은 Visual Studio 2008 명령 프롬프트에서 실행해야 하며 VC 2008 Express Editions(http://www.microsoft.com/express/download/)가 설치되어 있어야 한다.
cl -I"<JDK_HOME>\include" -I"<JDK_HOME>\include\win32" -LD atinjni.c -Featinjni.dll
cl : vc++ 컴파일 명령어
-I<dir> : 헤더 파일을 검색할 디렉토리 경로 <dir> 추가
-LD : DLL 생성
-FE<파일명> : 컴파일 결과 파일 이름 지정
나는 VC에서 직접하고 싶어서 [1]과 같이 dll을 만들었다.
그런데 다음과 같은 문제에 직면했다.
Can't load IA 32-bit .dll on a AMD 64-bit platform
그래. 나는 64비트용 윈도우 7 운영체제였 다. 32비트 dll이 문제가 되는 것이었다. 구글링해보니 32비트용으로 Java를 다시 까니 실행 명령어에 옵션을 주는 몇 방법들이 있었는데 마음에 들지 않았다. dll을 64비트용으로 하면 될 것 같았다. 그래서 [2]와 같은 방법으로 dll을 64비트용으로 만들어주었고 문제를 해결했다.
6. Java 프로그램 실행
이클립스라면 그냥 실행하면 되고 콘솔에서라면 "java AtinJNI"로 실행하면 결과를 볼 수 있다.
Reference
[1] VC에서 DLL 만드는 방법 (http://sol9501.blog.me/70102942944)
[2] 64비트용 컴파일 (http://blog.naver.com/PostView.nhn?blogId=honnak&logNo=70085595801&viewDate=¤tPage=1&listtype=0)
[3] 인사이드 안드로이드(Inside the Android Framework, 위키북스)
=======================
=======================
=======================
'JAVA' 카테고리의 다른 글
자바 파일스트림 쓰기 (0) | 2020.09.18 |
---|---|
이클립스 java Class Path 관련 (0) | 2020.09.18 |
[Java] 다른 프로그램 프로세싱 관리, 프로그램 죽으면 다시 실행 시키기 관련, 자바로 외부파일 실행. (0) | 2020.09.18 |
자바 현재시간 날짜 구하기 관련 (0) | 2020.09.18 |
java.lang.IllegalArgumentException: white space not allowed 관련 java 버그?? (0) | 2020.09.18 |