[MinGW ] 윈도우용 SQLCipher C에서 자바까지 - 2
[MinGW ] 윈도우용 SQLCipher C에서 자바까지 - 2 Sqlcipherjdbc 드라이버 만들어 보기 안드로이...
blog.naver.com
Sqlcipherjdbc 드라이버 만들어 보기
안드로이드 라이브러리는 sqlcipher 제작사에서 제공 하지만 jdbc 같은 경우에는 만들기가 까다롭다고나 할까? 이제부터 sqlcipher 를 위한 jdbc 라이브러리를 만들어 보기로 하자. 참고로 현재 사용한 모든 소스는 github에 있다.
git clone https://github.com/decamp/sqlcipher-jdbc
|
어차피, 커스텀으로 시작한 것이므로jdbc 라이브러리를 제작하기 위한 작업도 커스텀으로 시작해 보기로 한다. 우선 손에 익은 ant와 MinGW32로 만들어 가보기로 하자.
제작 순서는 다음과 같다.
1. OpenSSL 빌드 및 설치
2. Sqlcipher 빌드 및 설치 3. JNI 빌드 및 라이브러리 빌드 4. Jdbc 소스 빌드 5. 테스트 |
우선 1~2의 경우에는 저번에 했던 내용에서 바뀐 내용은 없을 듯하다. 그러면 3 ~ 5 번 순으로 빌드를 수행하여 지행해보도록 한다.
1. JNI 빌드 및 라이브러리 빌드
빌드를 한다는 것이 일일이 수작업을 통해서 해 줄 수는 있으나, 나중을위해서 약간의 자동화가 필요하다는 생각에 Ant로 빌드를 수행 하도록 한다.
다음은 build.xml 파일 전문이다.
<?xml version="1.0" encoding="UTF-8"?>
<project name="sqlcipher-jdbc" default="sqlcipher-jdbc" basedir=".">
<description>
sqlcipher jdbc
</description>
<!-- set global properties for this build -->
<property name="version" value="0.1"/>
<property name="manifest-version" value="0.1"/>
<property name="src" location="src"/>
<property name="build" location="build"/>
<property name="dist" location="dist"/>
<property name="support.lib.dir" location="lib"/>
<property name="bin.dir" location="bin"/>
<property name="exec.dir" location="exec"/>
<property name="core.dir" location="org/sqlite/core"/>
<property name="ext.dir" location="org/sqlite/core"/>
<property name="native.dir" location="native"/>
<property name="native.ab.dir" location="C:/DEV/COMP/msys32/home/tobee/sqlcipher-build/sqlcipher-jdbc-ant/native"/>
<property name="native.lib.name" value="sqlitejdbc.dll"/>
<property name="native.package" location="org/sqlite/native/Windows/x86"/>
<property name="manifest" value="MANIFEST.MF"/>
<property name="main.class" value="org.sqlite.core.NativeDB"/>
<property name="main.name" value="sqlcipher-jdbc"/>
<property name="jar.name" value="sqlcipher-jdbc"/>
<property name="username" value="tobee"/>
<patternset id="native.files">
<include name="**/*.dll" />
</patternset>
<path id="compile.classpath">
<fileset dir="${support.lib.dir}">
<include name="**/*.jar"/>
</fileset>
</path>
<target name="init">
<!-- Create the time stamp -->
<tstamp/>
<!-- Create the build directory structure used by compile -->
<mkdir dir="${build}"/>
</target>
<target name="compile" depends="init" description="compile the source " >
<javac srcdir="${src}" destdir="${build}" source="1.7" debug="true" encoding="UTF-8">
<classpath refid="compile.classpath"/>
</javac>
</target>
<target name="dist" depends="compile" description="generate the distribution" >
<!-- Create the distribution directory -->
<mkdir dir="${dist}/lib"/>
<delete file="${current.lib.dir}/${jar.name}.jar"/>
<jar destfile="${dist}/lib/${jar.name}.jar"
basedir="${build}"
manifest="${manifest}">
<manifest>
<attribute name="Main-Class" value="${main.class}"/>
<attribute name="Built-By" value="${username}"/>
<attribute name="Build-Jdk" value="jdk1.8.0_111"/>
</manifest>
<fileset dir="${native.dir}">
<patternset refid="native.files" />
</fileset>
</jar>
<copy todir="${bin.dir}">
<fileset dir="${dist}/lib">
<include name="${jar.name}.jar"/>
</fileset>
</copy>
<copy todir="${exec.dir}">
<fileset dir="${dist}/lib">
<include name="${jar.name}.jar"/>
</fileset>
</copy>
</target>
<target name="gen-jni">
<echo message="generating JNI header" />
<javah outputFile="${native.dir}/NativeDB.h" classpath="${build}" >
<class name="org.sqlite.core.NativeDB" />
</javah>
</target>
<property name="test.build.dir" value="tests/build"/>
<property name="test.src.dir" value="tests/src"/>
<path id="classpath.test">
<pathelement location="lib/junit-4.5.jar"/>
<pathelement location="${dist}/lib/${jar.name}.jar"/>
</path>
<target name="test-compile" depends="compile">
<mkdir dir="${test.build.dir}"/>
<javac srcdir="${test.src.dir}" destdir="${test.build.dir}" includeantruntime="false">
<classpath refid="classpath.test"/>
</javac>
</target>
<target name="test" depends="test-compile">
<copy todir="${test.build.dir}/org/sqlite">
<fileset dir="${test.src.dir}/org/sqlite">
<include name="testdb.jar"/>
<include name="sample.db"/>
</fileset>
</copy>
<echo message="library..${native.ab.dir}"/>
<junit printsummary="on" haltonfailure="yes" fork="true">
<classpath>
<path refid="classpath.test"/>
<pathelement location="${test.build.dir}"/>
</classpath>
<sysproperty key="org.sqlite.lib.path" path="${native.ab.dir}"/>
<sysproperty key="org.sqlite.lib.name" value="${native.lib.name}"/>
<formatter type="brief" usefile="false" />
<batchtest>
<fileset dir="${test.src.dir}" includes="**/*Test.java" />
</batchtest>
</junit>
</target>
<target name="sqlcipher-jdbc" depends="dist"
description="make own jar file" >
</target>
<target name="clean" description="clean up" >
<!-- <echo message="Hello, bin/${jar.name}.jar"/> -->
<delete dir="${build}"/>
<delete dir="${dist}"/>
<delete file="bin/${jar.name}.jar"/>
</target>
<target name="test-clean" description="test clean up" >
<delete dir="${test.build.dir}"/>
</target>
</project>
뭐 눈 여겨 볼 사항은 jni 생성 부분인데 아래와 같다.
<target name="gen-jni">
<echo message="generating JNI header" />
<javah outputFile="${native.dir}/NativeDB.h" classpath="${build}" >
<class name="org.sqlite.core.NativeDB" />
</javah>
</target>
파일 명은 NativeDB.h 로 하는 데 원래 사이트에서도 동일한파일 명으로 생성 되는 듯 하다.
그런 다음 테스트를 위해서 junit을 사용하게 되는 데 Ant에서도 JUNIT을 지원 해 준다. 다음을 살펴 보면 될 듯 하다.
<target name="test" depends="test-compile">
<copy todir="${test.build.dir}/org/sqlite">
<fileset dir="${test.src.dir}/org/sqlite">
<include name="testdb.jar"/>
<include name="sample.db"/>
</fileset>
</copy>
<echo message="library..${native.ab.dir}"/>
<junit printsummary="on" haltonfailure="yes" fork="true">
<classpath>
<path refid="classpath.test"/>
<pathelement location="${test.build.dir}"/>
</classpath>
<sysproperty key="org.sqlite.lib.path" path="${native.ab.dir}"/>
<sysproperty key="org.sqlite.lib.name" value="${native.lib.name}"/>
<formatter type="brief" usefile="false" />
<batchtest>
<fileset dir="${test.src.dir}" includes="**/*Test.java" />
</batchtest>
</junit>
</target>
<sysproperty key="org.sqlite.lib.path" path="${native.ab.dir}"/>
<sysproperty key="org.sqlite.lib.name" value="${native.lib.name}"/>
자바를 빌드 한 후에 소스 폴더에 있는 리소스 파일 들을 빌드 폴더에 옮겨 주고 난 다음, sqljdbc 라이브러리 중 JNI로 묶어 줄 라이브러리 경로는설정 해 주는 부분에 유의 해 주면 될 듯 하다. 실제 소스 코드를 만들 때도 위의 부분은 중요 한사항이다.
2. 폴더 구조
폴더 구조는 다음과 같다.


여러가지 잡다한 폴더가 많지만 그 중에서 src/tests/native 폴더만중요하다.
그리고 난 다음에 src 폴더는 기존 src/main 에 있는 내용을 복사하고, tests에는 src/test 내용을 복사해서 사용하였으며, native 폴더는파일을 찾아서 다음과 같이 구성 하였다.
3. 빌드 절차
1. 자바 빌드
2. JNI 헤더 생성 3. 라이브러리 빌드 4. 테스트 |
3.1 자바 빌드
자바를빌드 한 후에는 javah 명령을 사용해서 헤더 파일을 만들어 낼 수 있도록 한다.
Ant
Ant gen-jni
3.2 라이브러리 빌드
라이브러리빌드는 위의 파일이 다 구성 되었을 경우 Mingw로 빌드 할 수 있다. 다음은 Makefile 전문이다.
MYINCL = ./ -Wl,--kill-at
MYBIN = .
OPENSSL_HOME = /home/tobee/openssl
CIPHER_HOME = /home/tobee/sqlcipher
CIPHER_SRC = /home/tobee/sqlcipher-build/sqlcipher
CIPHER_BULD = /home/tobee/sqlcipher-build/sqlcipher/build
JAVA_HOME = /C/DEV/COMP/Java/jdk1.8.0_111
CC = i686-w64-mingw32-gcc
AR = ar
RM = rm -Rf
CP = cp
STRIP= strip
ECHO = echo
ARFLAGS = -ruv
JNI = -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/win32 -O2
SQLITEDEF= -DSQLITE_HAS_CODEC \
-DSQLITE_HAVE_ISNAN \
-DSQLITE_ENABLE_COLUMN_METADATA \
-DSQLITE_OMIT_LOAD_EXTENSION \
-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT \
-DSQLITE_ENABLE_COLUMN_METADATA \
-DSQLITE_CORE -DSQLITE_ENABLE_FTS3 \
-DSQLITE_ENABLE_FTS3_PARENTHESIS \
-DSQLITE_ENABLE_RTREE \
-DSQLITE_ENABLE_STAT2
TSCFLAG = -Wl,--kill-at -shared -Wl,--out-implib,$(MYBIN)/libsqlitejdbc.a
CIPHERINCL= -I/mingw32/include -I$(OPENSSL_HOME)/include -I$(CIPHER_HOME)/include/sqlcipher -I$(CIPHER_SRC) -I$(CIPHER_SRC)/src -I$(CIPHER_BULD)
TISLIB = -L$(CIPHER_HOME)/lib -lsqlcipher -L$(OPENSSL_HOME)/lib -lcrypto -static-libgcc -lgdi32
CFLAGS = $(SQLITEDEF) $(JNI) $(TSCFLAG) $(CIPHERINCL)
SHAREDLIB = $(MYBIN)/sqlitejdbc.dll
all: $(SHAREDLIB)
#OBJS = NativeDB.o extension-functions.o
OBJS = extension-functions.o NativeDB.o
copy:
@$(RM) org/sqlite/native/Windows/x86/*.dll
@$(CP) *.dll org/sqlite/native/Windows/x86/
clean:
@$(RM) $(SHAREDLIB) $(OBJS) core
@$(ECHO) "clean $(SHAREDLIB) $(OBJS) OK ..."
.SUFFIXES: .c .cpp .o
.c.o:
@$(ECHO) "@$(CC) -c $(CFLAGS) $< -o $@"
@$(CC) -c $(CFLAGS) $< -o $@
@$(ECHO) "$< compile ..."
$(SHAREDLIB) : $(OBJS)
@$(ECHO) "$@ Program Linking"
@$(ECHO) "@$(CC) $(TSCFLAG) -o $@ $(OBJS) $(TISLIB)"
@$(CC) $(TSCFLAG) -o $@ $(OBJS) $(TISLIB)
@$(RM) $(OBJS)
@$(STRIP) $(SHAREDLIB)
빌드하기
빌드시에주의 할 점은 의존성이 걸리는 gcc 라이브러리를 정적으로 링크 한다는 것이다.

3.3 테스트하기
로그파일로 대체한다.
ant test
Buildfile: C:\DEV\COMP\msys32\home\tobee\sqlcipher-build\sqlcipher-jdbc-ant\build.xml init: compile: [javac] C:\DEV\COMP\msys32\home\tobee\sqlcipher-build\sqlcipher-jdbc-ant\build.xml:51: warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds test-compile: test: [echo] library..C:\DEV\COMP\msys32\home\tobee\sqlcipher-build\sqlcipher-jdbc-ant\native [junit] Running org.sqlite.BackupTest [junit] Testsuite: org.sqlite.BackupTest [junit] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.372 sec [junit] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.372 sec [junit] [junit] Running org.sqlite.ConnectionTest [junit] Testsuite: org.sqlite.ConnectionTest [junit] Tests run: 13, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.088 sec [junit] Tests run: 13, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.088 sec [junit] [junit] Running org.sqlite.DBMetaDataTest [junit] Testsuite: org.sqlite.DBMetaDataTest [junit] Tests run: 24, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.096 sec [junit] Tests run: 24, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.096 sec [junit] [junit] Running org.sqlite.EncryptedTest [junit] Testsuite: org.sqlite.EncryptedTest [junit] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.982 sec [junit] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.982 sec [junit] [junit] Running org.sqlite.ExtendedCommandTest [junit] Testsuite: org.sqlite.ExtendedCommandTest [junit] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.009 sec [junit] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.009 sec [junit] [junit] Running org.sqlite.ExtensionTest [junit] Testsuite: org.sqlite.ExtensionTest [junit] Tests run: 2, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.042 sec [junit] Tests run: 2, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.042 sec [junit] [junit] Testcase: extFunctions(org.sqlite.ExtensionTest):SKIPPED [junit] Running org.sqlite.FetchSizeTest [junit] Testsuite: org.sqlite.FetchSizeTest [junit] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.042 sec [junit] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.042 sec [junit] [junit] Running org.sqlite.InsertQueryTest [junit] Testsuite: org.sqlite.InsertQueryTest [junit] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.562 sec [junit] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.562 sec [junit] [junit] Running org.sqlite.JDBCTest [junit] Testsuite: org.sqlite.JDBCTest [junit] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.046 sec [junit] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.046 sec [junit] [junit] Running org.sqlite.OSInfoTest [junit] Testsuite: org.sqlite.OSInfoTest [junit] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.012 sec [junit] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.012 sec [junit] [junit] Running org.sqlite.PrepStmtTest [junit] Testsuite: org.sqlite.PrepStmtTest [junit] Tests run: 28, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.093 sec [junit] Tests run: 28, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.093 sec [junit] [junit] Running org.sqlite.QueryTest [junit] Testsuite: org.sqlite.QueryTest [junit] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.055 sec [junit] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.055 sec [junit] [junit] Running org.sqlite.RSMetaDataTest [junit] Testsuite: org.sqlite.RSMetaDataTest [junit] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.055 sec [junit] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.055 sec [junit] [junit] Running org.sqlite.ReadUncommittedTest [junit] Testsuite: org.sqlite.ReadUncommittedTest [junit] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.047 sec [junit] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.047 sec [junit] [junit] Running org.sqlite.SQLiteConfigTest [junit] Testsuite: org.sqlite.SQLiteConfigTest [junit] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.013 sec [junit] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.013 sec [junit] [junit] Running org.sqlite.SQLiteConnectionPoolDataSourceTest [junit] Testsuite: org.sqlite.SQLiteConnectionPoolDataSourceTest [junit] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.052 sec [junit] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.052 sec [junit] [junit] Running org.sqlite.SQLiteDataSourceTest [junit] Testsuite: org.sqlite.SQLiteDataSourceTest [junit] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.048 sec [junit] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.048 sec [junit] [junit] Running org.sqlite.SQLiteJDBCLoaderTest [junit] Testsuite: org.sqlite.SQLiteJDBCLoaderTest [junit] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.044 sec [junit] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.044 sec [junit] [junit] Running org.sqlite.StatementTest [junit] Testsuite: org.sqlite.StatementTest [junit] Tests run: 31, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.09 sec [junit] Tests run: 31, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.09 sec [junit] [junit] Testcase: ambiguousColumnNaming(org.sqlite.StatementTest):SKIPPED [junit] Running org.sqlite.TransactionTest [junit] Testsuite: org.sqlite.TransactionTest [junit] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.75 sec [junit] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.75 sec [junit] [junit] ------------- Standard Output --------------- [junit] running in native mode [junit] ------------- ---------------- --------------- [junit] Running org.sqlite.UDFTest [junit] Testsuite: org.sqlite.UDFTest [junit] Tests run: 16, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.173 sec [junit] Tests run: 16, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.173 sec [junit] BUILD SUCCESSFUL Total time: 10 seconds |
4. 개발 환경 설정
그럼 이클립스에서 개발하기 위한 환경을 설정을 해 보도록 한다. 우선 사용자 라이브러리를 하나 만들고 – sqlcipher-jdbc – 이라이브러리의 “Native library location”에 해당dll의 위치를 잡아 주도록 한다.

그런 다음 테스트 클래스를 하나 작성 해보도록 하자.
테스트로 데이터베이스를 하나 만들어 놨다고 가정하겠다.
5. SqlcipherTest 클래스
다음 클래스 전문을 보도록 한다.
package com.tobee.prepare.test;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class SqlcipherTest {
private static File testdb = new File("resources/db/mydb.db");
private static final String QUERY_GPS_DATA =
"SELECT USR_ID, IMEI, LAT, LON, GDATE,GTIME " +
"FROM GPS_TRACK";
public static void forName() throws Exception {
String sqliteNativeLibraryPath =
"C:/DEV/COMP/msys32/home/tobee/sqlciper-jdbc/bin";
String sqliteNativeLibraryName = "sqlitejdbc.dll";
System.setProperty("org.sqlite.lib.path", sqliteNativeLibraryPath);
System.setProperty("org.sqlite.lib.name", sqliteNativeLibraryName);
Class.forName("org.sqlite.JDBC");
}
private static Connection getConnection() throws SQLException {
if(!testdb.exists())
System.out.println("test db doesn't exist...!!!");
else
System.out.println("test db exists...!!!");
String url = "jdbc:sqlite:" + testdb.getAbsolutePath();
Properties props = new Properties();
Connection conn = DriverManager.getConnection(url, props);
return conn;
}
private static void queryCommnGPSData() {
Connection conn = null;
try {
// create a database connection
conn = getConnection();
conn.setAutoCommit(true);
Statement statement = conn.createStatement();
statement.setQueryTimeout(30); // set timeout to 30 sec.
statement.execute("PRAGMA key ='password'");
ResultSet rs =
statement.executeQuery(QUERY_GPS_DATA);
while (rs.next()) {
System.out.println("USR_ID: " + rs.getString(1));
System.out.println("IMEI: " + rs.getString(2));
System.out.println("LAT: " + rs.getString(3));
System.out.println("LON: " + rs.getString(4));
System.out.println("GDATE: " + rs.getString(5));
System.out.println("GTIME: " + rs.getString(6));
}
} catch (SQLException e) {
//System.err.println(e.getMessage());
e.printStackTrace();
} finally {
try {
if (conn != null)
conn.close();
} catch (SQLException e) {
// connection close failed.
System.err.println(e);
}
}
}
public static void main(String[] args)
{
try {
forName();
} catch (Exception e) {
e.printStackTrace();
}
queryCommnGPSData();
}
}
주요 부분만 살펴 본다면 forName 메서드에서 프로퍼티 값을 설정해 주는 부분과
public static void forName() throws Exception {
String sqliteNativeLibraryPath =
"C:/DEV/COMP/msys32/home/tobee/sqlciper-jdbc/bin";
String sqliteNativeLibraryName = "sqlitejdbc.dll";
System.setProperty("org.sqlite.lib.path", sqliteNativeLibraryPath);
System.setProperty("org.sqlite.lib.name", sqliteNativeLibraryName);
Class.forName("org.sqlite.JDBC");
}
암호화 된 데이터 베이스를 접근 하는 부분이 되겠다.
statement.execute("PRAGMA key ='password'");
사이트에 나온 데로 구성 한 경우에 데이터베이스에 잘 접근이 안되는 부분이 있는 그 이유는 잘 모른 것으로 ….
6. 결론
이렇게 해서 Sqlcipher를 통해서 접근 하는 모든 내용을 한번 돌아 본 것 같다. 꾸준히 사용해 볼 기회가 있으면 좋겠는 데, 어떻게될 지는 잘 모르겠다.
이상.
'프로그래밍' 카테고리의 다른 글
[C#] 인라인 함수 사용하기 (0) | 2023.04.04 |
---|---|
[자바] 부울 값을 정수 값으로 변환하기 (0) | 2023.04.03 |
[MinGW ] 윈도우용 SQLCipher C에서 자바까지 (0) | 2023.03.30 |
slicing 이해하기 (0) | 2023.03.29 |
[C#] 헥사 문자열을 Int 로 변환 (2) | 2023.03.29 |