Cảm ơn bạn đã đọc và ủng hộ blog KTMT ʘ‿ʘ Từ bây giờ chúng tôi sẽ là kipalog.com !

Do công việc, tôi đã có một thời gian tìm hiểu về OpenCV để xử lý ảnh và video. Phần lớn các tác vụ tôi thực hiện là code OpenCV trên C++ và giao tiếp với Android qua JNI. Và rồi tôi nhận ra mình chỉ làm theo những tutorial có sẵn trên mạng và mất nhiều thời gian hơn mức cần thiết để fix bug của compiler hoặc IDE. Bản thân tôi rất có hứng thú với Computer Vision và Image Processing nói chung, nhưng cách tiếp cận như trên có vẻ như quá chậm. Sẽ tốt hơn nếu có thể tập trung nắm được những khái niệm cơ bản cùa Computer Vision trước, không bị sa đà vào việc fix bug. Bản thân việc nhanh chóng tạo ra những ứng dụng hữu ích là một động lực không nhỏ đối với việc tiếp tục học Computer Vision and Image Processing.

Đó là lý do tôi chuyển sang lập trình OpenCV với Python. Việc cài đặt, viết code, thử nghiệm đều trở nên đơn giản hơn rất nhiều.

Thêm một lý do nữa, phải nói là Computer Vision và Image Processing là những lĩnh vực khó nếu xét về khía cạnh nghiên cứu, với rất nhiều công thức toán. Tôi không nói những công thức toán đó là không cần thiết, mà là đối với người bắt đầu học, sẽ tốt hơn nếu nhanh chóng nắm được tất cả những kĩ thuật cơ bản và bắt đầu áp dụng xây dựng ứng dụng hữu ích cho mình. Sau đó, tuỳ theo nhu cầu và sở thích, có thể tiếp tục đào sâu nghiên cứu, tối ưu, chạy thời gian thực… Kể cả lúc đó, khi đã có kiến thức cơ bản rồi, việc tiếp cận sẽ càng nhanh hơn.

Xin lỗi vì phần giới thiệu hơi dài dòng :P Tôi xin bắt đầu luôn. Trong bài đầu tiên này, tôi sẽ trình bày cách cài đặt môi trường và một số tác vụ cơ bản khi làm việc với OpenCV trên Python. Bài viết này có thể sẽ khá cơ bản với một số độc giả, nhưng vì là bài đầu tiên trong loạt bài này, tôi vấn muốn trình bày để đảm bảo sự đầy đủ. Trong các bài sau, chúng ta sẽ cùng xây dựng những ứng dụng thú vị hơn.

Cài đặt môi trường

Có những thành phần sau cần phải cài đặt để làm việc với OpenCV trên : * Python 2.7 * NumPy và SciPy: Bằng cách sử dụng NumPy, chúng ta có thể biểu thị hình ảnh bằng mảng đa chiều. Ngoài ra, có rất nhiều thư viện xử lý ảnh và cả machine learning sử dụng cách biểu thị mảng của NumPy. NumPy cũng hỗ trợ rất nhiều hàm toán học chúng ta có thể thực hiện nhiều phân tích có ý nghĩa hơn trên ảnh. Bên cạnh NumPy còn có SciPy đi kèm, hỗ trợ thêm về các tính toán khoa học.

Trên Windows, cách dễ nhất để có cả Python, NumPy và SciPy là cài đặt Enthought Canopy tại https://www.enthought.com/products/canopy/

Trên Mac từ 10.7 trở lên, NumPy và SciPy đã được cài đặt sẵn. * Matplotlib: là thư viện để plot. Khi xử lý ảnh, chúng ta sẽ sử dụng thư viện này để xem histogram của ảnh hoặc xem chính ảnh đó. * OpenCV: Về hướng dẫn cài đặt chi tiết, bạn có thể xem tại đây

Mở và lưu file ảnh

Hãy bắt đầu bằng tác vụ đơn giản nhất: mở một file ảnh thành một mảng NumPy, đọc các thông tin về kích cỡ, sau đó lưu mảng NumPy thành một file ảnh mới.

getinfo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import argparse
import cv2

ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required = True,
                help = "Path to the image")
args = vars(ap.parse_args())

image = cv2.imread(args["image"])
print "width: %d pixels" % (image.shape[1])
print "height: %d pixels" % (image.shape[0])
print "channels: %d channels" % (image.shape[2])

cv2.imshow("Image", image)
cv2.imwrite("new.jpg", image)
cv2.waitKey(0)

Đầu tiên, chúng ta import hai package cần thiết là argparsecv2. argparse dùng để khai báo những parameter cần thiết cho chương trình của chúng ta, ở đây, chúng ta khai báo parameter là đường dẫn đến file ảnh ban đầu. Để đọc file ảnh thành một mảng NumPy, chúng ta sử dụng hàm cv2.imread() với parameter là tên file ảnh (đã được nhập vào từ command line). Sau bước này, ta nhận được một mảng NumPy là image. Tiếp theo ta in ra các thông số về file ảnh, gồm có chiều dài, chiều rộng và số channel. Ở đây, chúng ta có những điểm lưu ý về cách NumPy lưu các thông số về bức ảnh: * Một bưc ảnh có 2 chiều X và Y, gốc toạ độ tại pixel trên cùng bên trái của bức ảnh. Chiều X từ trái sang phải và chiều Y từ trên xuống dưới. NumPy lưu số pixel hơi ngược: image.shape[0] là số pixel theo chiều Y và image.shape[1] là số pixel theo chiều X. * Mỗi pixel trên bức ảnh được biểu thị dưới một trong 2 dạng: grayscale hoặc color. image.shape[2] lưu số channel biểu thị mỗi pixel. Với ảnh màu hiển thị trên RGB, số channel là 3, còn với ảnh đen trắng (grayscale), chúng ta chỉ có 1 channel duy nhất

Để hiển thị bức ảnh trên một window mới, ta sử dụng hàm cv2.imshow(), với 2 đối số là tên của window và tên của mảng NumPy muốn hiển thị.

Để lưu mảng NumPy thành một file ảnh mới, ta sử dụng hàm cv2.imwrite(), với 2 đối số là tên của file muốn lưu và tên của mảng NumPy. Trong ví dụ này, chúng ta lưu file mới giống hệt file cũ mà không có chỉnh sửa gì.

Chạy đoạn code trên như sau:
1
% python getinfo.py -i obama_fun.jpg

Ảnh gốc:

Kết quả trả về
1
2
3
width: 475 pixels
height: 356 pixels
channels: 3 channels

Tạo ảnh avatar bằng kĩ thuật mask

Trong phần này, chúng ta sẽ tạo ra ảnh avatar từ một bức hình chân dung ban đầu, theo dạng như avatar picture của Google+: hình tròn bao quanh khuôn mặt. Đoạn code như sau:

create_avatar_circle.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np
import argparse
import cv2

ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required = True,
                help = "Path to the image")
args = vars(ap.parse_args())

image = cv2.imread(args["image"])
cv2.imshow("Original", image)

(cX, cY) = (image.shape[1] / 2 , image.shape[0]/2)
radius = 120
mask = np.zeros(image.shape[:2], dtype = "uint8")
cv2.circle(mask, (cX, cY), radius, 255, -1)
masked = cv2.bitwise_and(image, image, mask = mask)
avatar = masked[ cY - radius : cY + radius, cX - radius : cX + radius]
cv2.imshow("Mask", mask)
cv2.imshow("Avatar", avatar)
cv2.imwrite("avatar.jpg", avatar)
cv2.waitKey(0)

Đoạn code bắt đầu với các thủ tục như import packages cần thiết, khai báo các parameters đầu vào cho script, đọc file ảnh vào mảng NumPy như phần trên.

Để crop một phần bức ảnh, chúng ta cần tạo ra một mặt nạ mask, là một mảng có kích thước như bức ảnh với tất cả giá trị pixel được khởi tạo bằng 0. Tiếp đó, ta vẽ một hình tròn trắng trên mảng ‘mask’ . Sau đó, sử dụng bitwise_and() hai mảng image với nhau, có thêm tham số mặt nạ mask, ta được một bức ảnh masked chỉ có phần khuôn mặt. Tiếp tục crop bằng cách slice mảng NumPy, chúng ta được kết quả cuối cùng là bức ảnh avatar.

Chạy đoạn code trên như sau:
1
% python create_avatar_circle.py -i obama_fun.jpg

Kết quả:

  • Bức ảnh ban đầu

  • mask

  • avatar

Tất nhiên, tôi đã sử dụng file ảnh có mặt tổng thống Obama ở ngay giữa tấm ảnh và mask được đặt ở chính giữa tâm bức ảnh. Không phải mọi tấm ảnh đều như vậy; trong trường hợp đó chúng ta sẽ sử dụng kĩ thuật face detection. Nhưng đó là chủ đề của một bài viết sau.

Kết luận

Qua bài viết này, tôi đã giới thiệu những bước bắt đầu để làm việc với OpenCV qua Python. Bài viết khá cơ bản, nhưng đã trình bày một số khái niệm cơ bản để chuẩn bị cho những ứng dụng lần sau. Hẹn gặp lại các bạn trong các phần tiếp theo.

Tài liệu tham khảo

  1. OpenCV-Python Tutorials
Comments

Thời gian vừa rồi blog ktmt có tạo một bản điều tra về các thói quen của lập trình viên ở Việt Nam nói chung. Bản điều tra nằm ở đường link:

https://ktmt.typeform.com/to/WzOLiB

Những bạn nào vẫn chưa làm mà có hứng thú làm thì nhóm bọn mình vẫn hết sức welcome.

Bản điều tra có mục đích chính là để tìm hiểu thực trạng và thói quen của các lập trình viên Việt Nam, qua đó điều chỉnh các bài viết của blog sao cho có ích với càng nhiều người càng tốt.Ở bài viết này mình sẽ tổng kết về một số con số mà mình cảm thấy thú vị, hy vọng có ích với mọi người.

Đầu tiên là tính đến thời điểm viết bài này, bản điều tra nhận được 256 câu trả lời trên tổng số 416 lượt visits (tỉ lệ là cứ 5 người xem thì 3 người trả lời). Thời gian trả lời trung bình mất tầm 4 phút, và tỉ lệ hoàn thành 100% bản điều tra là 62% (Tuy nhiên phần lớn các câu hỏi đều có tỉ lệ trả lời tiệm cận 99%, thế nên con số 62% này không có giá trị lắm)

Sau đây là một số kết quả thú vị, mình sẽ đi lần lượt các câu hỏi từ trên xuống:

    1. Câu hỏi số năm kinh nghiệm: Các lập trình viên của Việt Nam đều khá trẻ, chủ yếu từ 1-3 năm kinh nghiệm. Các dải kinh nghiệm còn lại trải đều và giảm dần theo số năm.
    1. Câu hỏi về ngôn ngữ lập trình thành thạo: Câu hỏi này có kết quả không ngạc nhiên khi kết quả trải rất đều ở tất cả các ngôn ngữ. Đứng đầu là 3 ngôn ngữ có thể coi là “main stream” nhất là Java, C, C++. Lý do theo cá nhân mình là 3 ngôn ngữ này được dạy nhiều nhất trong đại học Việt Nam. Tiếp đến là các ngôn ngữ để làm web gồm có PHP/Javascript/Ruby/Python.Một số bạn cũng đã tìm hiểu và nắm vững được các ngôn ngữ mới như Scala hay Haskell, rất đáng hoan ngênh.
    1. Câu hỏi về nền tảng đã từng lập trình không có gì bất ngờ khi đi đầu là Windows, tiếp đến là Web và mobile(Android/iOS)
    1. Câu hỏi về IDE: Các lập trình viên Việt Nam khá ưa chuộng sublime text, có lẽ do tính thân thiện của Editor này. Theo sau đó là Eclipse/Vim. Visual Studio về đích với vị trí thứ 4, phải chăng lập trình trên nền dotnet đã không còn là xu hướng.
    1. Câu hỏi về lĩnh vực yêu thích: Lĩnh vực được quan tâm nhất là Web, theo sau đó mới là mobile. Ngoài ra có một số không nhỏ các bạn quan tâm đến bảo mật hay data mining, rất thú vị.
    1. Có 81% các bạn ngoài công việc/trường lớp có làm các project cá nhân. Đây quả thật đây là một con số đáng khích lệ vô cùng.
    1. Github không có gì lạ chính là dịch vụ quản lý source code được yêu thích nhất(47%). Tuy nhiên có tới 25% các bạn quản lý source code chỉ trên máy cá nhân, mà việc này khá nguy hiểm khi máy tính hỏng hóc.
    1. Có 50% các bạn đã từng đặt câu hỏi trên Stackoverflow và 50% chưa từng. Có lẽ ngôn ngữ chính là rào cản lớn nhất của việc này.
    1. Ngược lại với kết quả ở <8>, có tới 85% câu trả lời cho các vấn đề lập trình được các bạn tìm thấy trên Stackoverflow. Có lẽ Stackoverflow chỉ được coi là một cái “kho” để tìm kiếm chứ vẫn chưa được coi là một “diễn đàn” để mọi người đóng góp ý kiến hay trả lời giúp người khác.
  • 10/11. 50% số bạn trả lời sau khi tìm được câu trả lời cho điều mình tìm kiếm thì không lưu lại, có lẽ mọi người cho rằng tìm được kết quả là được, không quan tâm lắm đến việc lần sau gặp lại lỗi đấy thì sẽ phải mất công tìm lại lần nữa.

    1. 70% các bạn trả lời là có viết blog kĩ thuật cá nhân! Đây đúng là một con số rất đáng mừng.
    1. Trong những bạn có viết blog thì 40% là để memo lại các lỗi đã gặp, còn lại là các đoạn code yêu thích hoặc nội dung khác.
    1. Có tới 61% các bạn ngại viết blog vì: “Cảm thấy năng lực chưa đủ”. Đây có lẽ là một lý do rất đáng e ngại, bởi vì mọi mọi người vẫn còn “sợ” bị người khác nhìn vào và chê kém.

Trên đây là một số con số mình thấy khá thú vị và đã tổng kết lại. Rất cám ơn mọi người đã làm bản điều tra giúp bọn mình! Hy vọng mọi người tiếp tục ủng hộ blog trong thời gian tới!!!!

Comments

1. Mở đầu

Gần đây ở công ty tôi có được giao một task khá hay. Công ty tôi có một game viết trên nền tảng android. Game đó viết bằng anđroid, tuy nhiên lại chủ yếu dùng web view để hiển thị. Mặc dù vậy, một số logic như là set session cho user, authenticate cho user thì lại nằm trên android.

Chắc các bạn cũng đã biết, android app được viết bằng java, dịch ra file dex, sau đó được phân phối trên google playstore dưới dạng file apk. Do đó, android app có một điểm yếu cố hữu mà mọi java app đều mắc phải, đó là bảo mật. Điểm yếu bảo mật ở đây là gì? Đó là việc mà mọi java app đều có thể được phân tích ngược (reverse engineer) rất dễ dàng. Việc này bắt nguồn từ bản chất java được dịch ra bytecode ở dạng khá “gần” với ngôn ngữ lập trình thông thường, và bytecode chứa đầy đủ các thông tin cần thiết để bạn có thể dịch lại nguyên vẹn lại chương trình gốc.

Vậy cái điểm yếu bảo mật này liên quan đến cái app tôi đang phụ trách thế nào? Như tôi vừa nói ở trên, trong cái game mà tôi đang phụ trách, logic authenticate cho user sẽ nằm trên phía android. Điều này có nghĩa là trên android app sẽ phụ trách:

  • Mã hoá uuid của người dùng, gửi lên server
  • Server sẽ nhận uuid đó, và gửi session key về cho user để user set vào cookie.

Chắc hẳn sẽ có bạn thắc mắc là qui trình xác thực này quá đơn giản. Đúng vậy, quy trình này quá đơn giản, dẫn đến là việc chỉ cần user A (người xấu) biết uuid của user B (người bị hại) thì A sẽ giả mạo được bất cứ hành động của B như là gửi đồ từ B cho A.

Vậy tại sao không làm một qui trình xác thực tốt hơn, như dùng thêm một token giống như onetime password mà chỉ user đó mới biết được, hay là làm cách nào để “giấu” uuid đi để cho user khác không biết. Đúng là nên như thế! Tuy nhiên vì một số lý do “lịch sử” của legacy code, mà chúng ta không thể thay đổi qui trình xác thực một cách dễ dàng như thế được.

Như vậy thì với flow code hiện tại thì với điểm yếu của android tôi đã nói ở trên thì một người có chút kiến thức lập trình có thể dễ dàng dịch ngược đoạn logic dùng để xác thực mà tôi đã nói ở trên. Mà trong đó có việc mã hoá uuid người dùng mà khi bạn nhìn được logic code thì mã hoá cũng bằng thừa. Lý do tại sao lại bằng thừa vì code hiện tại đang sử dụng “Symmetric Cryptography Algorithm”. Symmetric ở đây có nghĩa là thuật toán mã hoá đối xứng, mà điển hình gồm có những thuật toán như blowfish, AES, DES.

Nói một cách đơn giản thì các loại thuật toán symmetric thì bên gửi và bên nhận sẽ dùng cùng một key, cùng một intitialize vector (Các khái niệm này tôi sẽ trình bày kĩ hơn ở phần sau) , do đó chỉ cần dịch ngược được code thì user A (người xấu) sẽ có được key và initialize vector để tạo ra một request hợp lệ sử dụng uuid của user B.

Vậy thì chúng ta phải giải quyết vấn đề này thế nào? Sau một hồi thảo luận với công ty thì tôi nghĩ ra một giải pháp “chữa cháy” tạm thời, đấy là chuyển logic vào native code sử dụng ndk và C, mục đích để đạt được là:

“Giấu” đi logic mã hoá uuid người dùng, giấu cả các tham số ban đầu như key và initialize vector. Do đó mà user A sẽ không biết làm cách nào để tạo ra một request hợp lệ với uuid của user B.

Cách giải quyết này tại sao tôi nói là tạm thời, bởi vì user A nếu có thêm một chút hiểu biết về ndk thì sẽ biết được interface cung cấp ở ndk code sẽ được public ra ngoài, do đó thì vẫn có thể tận dụng được điểm này để tạo ra một request hợp lệ. Tuy nhiên do không nghĩ ra giải pháp khác nên tạm thời dùng cách này sẽ hạn chế được các hacker “gà mờ”.

Vậy để đi theo hướng đi này chúng ta cần phải tìm hiểu về 2 thứ đó là : Android NDK và cách để sử dụng các thuật toán mã hoá trên ndk (ở đây là sử dụng ngôn ngữ C), đó là openssl. Phần giới thiệu hơi dài dòng, nhưng đến đây các bạn đã nắm được tại sao tiêu đề bài viết lại là Android NDK và open SSL.

Dưới đây chúng ta sẽ đi lần lượt về 2 vấn đề cần giải quyết : Android NDK và OpenSSL

2. Android NDK

Android NDK là một kit phát triển giúp bạn có thể phát triển các phần mềm android mà dựa một phần trên các đoạn code viết trên C hoặc C++. Bạn sẽ cần đến NDK trong các sản phầm cần đến hiệu năng cao, mà khi đó các đoạn code được build ra binary sẽ phát huy hiệu năng tối đa. Các logic code được thực hiện trên ndk ở dứoi đây tôi sẽ gọi chung là native code.

Về cơ chế hoạt động của ndk, bạn có thể hiểu một cách đơn giản như trong hình vẽ dưới đây, app của bạn sẽ tiến hành giao tiếp với native code thông qua một interface gọi là JNI.

Một cách đơn giản, JNI là một bộ giao thức giao tiếp chuẩn của java, giúp cho java code có thể nói chuyện được với C/C++ code, có thể truyền dữ liệu giữa 2 bên.

Để tham khảo thêm về android ndk, các bạn có thể vào trang chủ của android tại Trang chủ của android. Dưới đây tôi sẽ tóm tắt các bước cần thiết để sử dụng được ndk.

Cài đặt

Cách cài đặt android ndk khá giống với sdk, tức là chỉ đơn thuần là bạn tải bộ ndk về, đặt vào đâu đó. Trong bộ NDK đó sẽ chứa đầy đủ các tool để có thể build được ndk native code từ C/C++ source (bao gồm build script và các file header cần thiết). Quá trình cài đặt có thể hiểu tóm gọn qua đoạn script dưới đây (chạy trên môi trường unix):

1
2
3
4
5
6
wget http://dl.google.com/android/ndk/android-ndk64-r10-darwin-x86.tar.bz2
tar yxvf android-ndk64-r10-darwin-x86.tar.bz2
mv android-ndk64-r10-darwin-x86 ~/
echo "export PATH=$PATH:/~/android-ndk64-r10-darwin-x86" >> ~/.bash_profile
echo "export ANDROID_NDK_ROOT=/Users/huydo/android-ndk64-r10-darwin-x86" >> ~/.bash_profile
source ~/.bash_profile

Sau khi chạy đoạn script trên thì android ndk đã được thêm vào path của hệ thống, giúp chúng ta có thể gõ các lệnh như ndk-build từ bất kì đâu

Sử dụng

Trong bộ ndk bạn down về có chứa sẵn khá nhiều ví dụ về cách sử dụng ndk, từ đơn giản (như hello world) cho đến các ví dụ phức tạp hơn như xử lý ảnh (mà phải thao tác gửi dữ liệu giữa android app và ndk app khá phức tạp). Các bạn có thể tham khảo các ví dụ đó để có cái nhìn thực tế về ndk program. Dưới đây tôi sẽ trình bày ngắn gọn về quá trình sử dụng của tôi.

Như ở hình ở trên thì các bạn thấy là android app và native code sẽ “nói chuyện” với nhau thông qua một “ngôn ngữ” chung gọi là jni. Như vậy sẽ có 2 khả năng xảy ra, dẫn đến 2 ngữ cảnh để sử dụng ndk:

    1. Viết một số logic code quan trọng ở phía native code, và các logic còn lại để ở phía android app như bình thường. Các giao tiếp sẽ được gọi từ phía java thông qua jni. Cách tiếp cận này thuận lợi ở chỗ là chúng ta tận dùng được mọi điểm mạnh của android frame work, và chỉ các logic nào thật cần thiết mới đưa vào native code.
    1. Viết “native activity”, tức là logic của activity như hiển thị, life cycle, gọi các activity khác.. sẽ được code toàn bộ ở trên phía native. Cách này thực tế khá ít sử dụng, thường sử dụng trong trường hợp mà dữ liệu quá khó để truyền đi truyền lại giữa bên java và native, thì việc code luôn cả activity trên native cũng là một lựa chọn cần thiết.

Ở bài viết này tôi sẽ đi theo hướng tiếp cận 1, để giải quyết bài toán theo hướng:

Đưa logic mã hoá uuid người dùng vào một file C, build ra binary và gọi logic đó trên phía java thông qua JNI.

Coding và build

Để đi theo hướng tiếp cận 1 như đã nói ở trên, chúng ta có thể dễ dàng hình dung công việc phải làm:

  • Step 1: Viết logic code mã hoá trên C, nhận đầu vào là 1 chuỗi mô tả uuid của người dùng, đầu ra là chuỗi đó dã được mã hoá.
  • Step 2: Build đoạn code đó thành một file thư viện động (.so file) và “Nhúng” file thư viện động đó vào trong android project
  • Step 3: Viết logic code gọi native code trên java.

Step 1: Cấu trúc của một file native code viết trên C

Thông thường, chúng ta sẽ tạo một folder tên là jni và đặt toàn bộ các đoạn code, header, các thư viện liên quan vào trong đó.

File native code viết trên C khá đơn giản, chỉ cần tóm gọn lại trong 2 bước:

  • include thư viện
  • Viết các hàm dựa trên convention của jni để tạo ra các “interface”, và phía java sẽ gọi được các “interface” này một cách khá dễ dàng

Một ví dụ hết sức về native code như dưới đây:

hello_jni.c
1
2
3
4
5
6
7
8
9
#innclude <string.h>
#include <jni.h>

jstring
Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
                                                  jobject thiz )
{
   return (*env)->NewStringUTF(env, "Hello from JNI ! ");
}

Các bạn để ý tên hàm của native code sẽ dễ dàng nhận thấy convention như trong hình dưới đây:

Nhờ có convention đó mà các bạn sẽ thấy việc gọi logic của hàm đó trên phía java sẽ dễ dàng hơn bao giờ hết.

Ngoài ra các bạn có thể để ý một số điểm đặc biệt ở một đoạn native code như dưới đấy:

  • Giá trị trả về ở đây là jstring, đó là một kiểu dữ liệu đặc biệt của jni, mà khi phía java gọi, thư viện jni sẽ thực hiện chuyển đổi (marshalling) giá trị này về kiểu String của java.
  • Biến JNIEvn* env, bạn có thể hình dung đây là một con trỏ trỏ đến VirtualMachine (Dalvik) của android, nhờ có env này mà chúng ta có thể thao tác ngược từ phía native, để có thể sử dụng được các logic phía android. Như trong đoạn code trên thì chúng ta có thể thấy nhờ có env mà chúng ta có thể tạo được một unicode string từ trong C code.

Step 2: Build đoạn code đó thành .so file

Để build được file native C mà chúng ta vừa viết ở trên, chúng ta cần làm 2 việc:

  • Tạo 2 file Android.mk và Application.mk trong thư mục jni mà chúng ta đã nhắc đến ở trên
  • Android.mk có nhiệm vụ “miêu tả” module với hệ thống build. Trong file này chúng ta sẽ viết là module chúng ta có những file gì, path ở đâu, sử dụng những thư viện khác nào (dependency). Trong một app có thể có nhiều file Android.mk khi mà chúng ta có nhiều module.
  • Application.mk sẽ có nhiệm vụ “miêu tả” app của chúng ta với hệ thống build. Thông thường trong file này chúng ta sẽ mô tả những modules mà app sẽ dùng, cũng như là mô tả về CPU architecture mà app sẽ hỗ trợ (mà điển hình gồm có ARM, x86 và MIPS)
  • Build sử dụng ndk-build hết sức đơn giản chỉ bằng việc gõ lệnh ndk-build ở trong folder hiện tại.

Sau khi sử dụng lệnh ndk-build để build thì kết quả build là các file .so sẽ được copy vào thư mục libs ở root folder theo như hình trên đây. Các bạn có thể thấy là tương ứng với mỗi kiến trúc CPU sẽ có một folder được tạo ra, trong mỗi folder đó lại có các file .so khác nhau chỉ dùng với duy nhất một kiến trúc nhất định.

Step 3: Viết logic code gọi native code trên java

Đã build xong thư viện tĩnh, chúng ta chỉ còn một công đoạn cuối cùng là sử dụng đoạn logic ở trên trong android code. Theo như ở trên đã nói, interface của jni code sẽ được sử dụng dựa theo convention mà gồm có: package name, class name và cfunction name. Điều đó có nghĩa là: đoạn code java trong android của bạn sẽ phải có package name, class name và function name y hệt như interface của jni, thì bạn mới sử dụng được logic đó.

Vậy thì theo như ví dụ của chúng ta ở đây thì chúng ta cần phải làm 3 việc:

  • package name của đoạn code phải là com/example/hellojni
  • Class name phải là HelloJni
  • Bạn phải định nghĩa một hàm tên là stringFromJNI để gọi được logic từ native code.
HelloJni.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.hellojni;

import android.app.Activity;
import android.widget.TextView;
import android.os.Bundle;

public class HelloJni extends Activity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        TextView  tv = new TextView(this);
        tv.setText( stringFromJNI() );
        setContentView(tv);
    }

    public native String  stringFromJNI();

    static {
        System.loadLibrary("hello-jni");
    }
}

Từ đoạn code trên chắc các bạn đã hình dung ra cách để gọi native code thế nào dựa vào hàm System.loadLibrary("hello-jni") và việc định nghĩa hàm thông qua directive native

Như vậy chúng ta đã tìm hiểu rất sơ qua về ndk. Trong phần tiếp theo, tôi sẽ đi vào phần chính mà tôi muốn nói đến, đó là giới thiệu về openssl và sử dụng openssl trên android ndk.

Copyright © 2015 kỹ thuật máy tính