[Swift] Cách sử dụng code Swift trong C/C++
Bài đăng này đã không được cập nhật trong 5 năm
Giới thiệu.
Là một sinh viên mới học về lập trình, mình rất thích mày mò các cách để có thể tạo sự giao tiếp giữa hai ngôn ngữ khác nhau. Với bài viết trước, mình đã viết cách chạy code C/C++
trong Swift
. Hôm nay sẽ là ngược lại, chạy code Swift
bên trong C/C++
. Bắt đầu thôi.
Một chương trình đơn giản.
Chắc ai cũng đã từng thấy qua flag -c
trong swiftc
dùng để tạo file object. Như thế ta sẽ thử liệu tạo file object có giúp C
đọc được hàm của Swift
hay không. Để kiểm tra tính năng này thì ta không nên dùng đến các thư viện hay hàm của Swift
. Với một hàm cơ bản:
// module.swift
func add(a: CInt, b: CInt) -> CInt {
return a + b
}
// main.c
#include <stdio.h>
int add(int, int);
int main() {
printf("%d + %d = %d\n", 3, 4, add(3, 4));
return 0;
}
Ta sẽ complie file module.swift
qua object file và sau đó compile file main.c
:
$ swiftc module.swift -c -o addmodule.o
$ gcc -o main main.c addmodule.o
Ta vướng ngay lỗi không tìm thấy _add
trong object file đó. Vậy ta sẽ phân tích object file đó có gì:
$ nm addmodule.o
0000000000000010 T _$s9addmodule3add1a1bs5Int32VAF_AFtF
0000000000000076 S ___swift_reflection_version
0000000000000000 T _main
U _memset
0000000000000078 s l_llvm.swift_module_hash
Ta thấy có hai hàm toàn cục: _main
là entry point và _$s9addmodule3add1a1bs5Int32VAF_AFtF
nhìn khá giống hàm add
ta đã viết. Trong khi đó hàm add
trong C
lại cần tới _add
. Qua tìm hiểu thì mình biết được có một attribute không chính thức, đó là @_cdecl
giúp ta xử lý vấn đề này:
// module.swift
@_cdecl("add")
func add(a: CInt, b: CInt) -> CInt {
return a + b
}
Sau đó thực hiện compile:
$ swiftc module.swift -c -o addmodule.o
$ nm addmodule.o
0000000000000020 T _$s9addmodule3add1a1bs5Int32VAF_AFtF
0000000000000086 S ___swift_reflection_version
0000000000000010 T _add
0000000000000000 T _main
U _memset
0000000000000088 s l_llvm.swift_module_hash
Và ta thấy nó đã tạo thêm _add
vào ta ta có thể compile file main.c
được rồi, nhưng nhớ thay hàm main
bằng hàm start
và set entry point là _start
:
// main.c
#include <stdio.h>
int add(int, int);
int start() {
printf("%d + %d = %d\n", 3, 4, add(3, 4));
return 0;
}
$ gcc -o main main.c addmodule.o -e _start
$ ./main
3 + 4 = 7
Tuyệt vời, vậy là ta có thể tạo ra một hàm Swift
đơn giản để có thể sử dụng trong C
rồi.
Một chương trình đơn giản nữa.
Ta đã có thể viết một chương trình C
chạy được một hàm trong Swift
, nhưng ta lại chưa thử sử dụng các hàm riêng của Swift
. Ta thử viết một chương trình in ra chữ Hello world!
bằng hàm print
:
// module.swift
@_cdecl("hello")
func hello() {
print("Hello world!")
}
// main.c
void hello();
int start() {
hello();
return 0;
}
Nhưng khi thử theo cách trên thì lại bị lỗi trong quá trình biên dịch. Lý do rất đơn giản, chúng ta chưa định nghĩa kiểu String
và hàm print
cho C
. Vậy ta phải làm thế nào, chẳng lẽ phải kéo libswiftCore.dylib
vào, cách này quá ngu. Với C++
, ta có thể wrap code và build thư viện động, vậy tại sao ta không làm thế với Swift
. May mắn là, swiftc
có hỗ trợ flag -emit-library
để giúp ta tạo ra một file thư viện động, thư viện động thì không có _main
nên ta có thể thay thế hàm start
thành main
:
// module.swift
@_cdecl("hello")
func hello() {
print("Hello world!")
}
// main.c
void hello();
int main() {
hello();
return 0;
}
$ swiftc -o hellomodule.dylib -emit-library module.swift
$ gcc -o main main.c hellomodule.dylib
Và ta ăn ngay lỗi không tìm thấy hàm _hello
, vậy ta cũng sẽ phân tích hellomodule.dylib
:
$ nm hellomodule.so
0000000000000e70 t _$s11hellomodule5helloyyF
U _$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC
U _$sSSN
U _$ss27_allocateUninitializedArrayySayxG_BptBwlFyp_Tg5
U _$ss5print_9separator10terminatoryypd_S2StF
0000000000000f10 t _$ss5print_9separator10terminatoryypd_S2StFfA0_
0000000000000f30 t _$ss5print_9separator10terminatoryypd_S2StFfA1_
0000000000000fb2 s ___swift_reflection_version
0000000000000e60 t _hello
U _swift_bridgeObjectRelease
U dyld_stub_binder
Ta thấy có hàm _hello
, tuy nhiên t
trước mặt _hello
nói rằng _hello
là hàm local
trong file thư viện động này, do đó, ta phải chuyển nó về public
:
@_cdecl("hello")
public func hello() {
print("Hello world!")
}
Và thử compile lại:
$ swiftc -o hellomodule.dylib -emit-library module.swift
$ gcc -o main main.c hellomodule.dylib
$ ./main
Hello world!
Một chương trình khó hơn.
Thật tuyệt vời! Ta thử làm thứ gì đó khó hơn chút, ví dụ một chương trình vẽ đồ thị đơn giản chẳng hạn.
Ta sẽ cùng Cocoa
để viết giao diện và thử tạo một API
để C
có thể sử dụng. Đầu tiên ta phải tạo một Delegate
:
class AppDelegate: NSObject, NSApplicationDelegate {
let mainWindow: NSWindow? = NSWindow(contentRect: NSRect(x: 500, y: 300, width: 700, height: 500), styleMask: [.titled, .closable, .miniaturizable], backing: .buffered, defer: false)
var mainController: NSWindowController?
func applicationDidFinishLaunching(_ notification: Notification) {
mainWindow?.backgroundColor = .white
mainWindow?.orderFrontRegardless()
mainController = NSWindowController(window: mainWindow)
mainController?.showWindow(mainWindow)
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
Sau đó ta sẽ viết một API
giúp C
có thể chạy các code này:
// module.swift
class AppDelegate: NSObject, NSApplicationDelegate {
...
}
let delegate = AppDelegate()
@_cdecl("run")
public func run() {
let app = NSApplication.shared
app.delegate = delegate
app.activate(ignoringOtherApps: false)
app.setActivationPolicy(.regular)
app.run()
}
// main.c
void run();
int main() {
run();
return 0;
}
Sau đó ta thử build và chạy thử, ta có kết quả sau:
Tuyệt vời! Ta sẽ thêm một view
sử dụng NSBezierPath
vào delegate
để có thể vẽ đồ thị:
import Cocoa
class DrawView: NSView {
var graphSet: [([NSPoint], NSColor, CGFloat)] = []
var maxX: CGFloat? = nil
var maxY: CGFloat? = nil
var minX: CGFloat? = nil
var minY: CGFloat? = nil
func addPointSet(_ xSet: [CGFloat], _ ySet: [CGFloat], color: NSColor, size: CGFloat) {
assert(xSet.count == ySet.count)
var temp: [NSPoint] = []
for i in 0..<xSet.count {
if maxX == nil || maxX! < xSet[i] {
maxX = xSet[i]
}
if maxY == nil || maxY! < ySet[i] {
maxY = ySet[i]
}
if minX == nil || minX! > xSet[i] {
minX = xSet[i]
}
if minY == nil || minY! > ySet[i] {
minY = ySet[i]
}
temp.append(NSPoint(x: xSet[i], y: ySet[i]))
}
graphSet.append((temp, color, size))
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
if maxX != nil && maxY != nil && minX != nil && minY != nil {
if maxX! != minX! && maxY! != minY! {
let xCoff = self.bounds.size.width/(maxX! - minX!)
let yCoff = self.bounds.size.height/(maxY! - minY!)
for e in graphSet {
let points = e.0
let pathColor = e.1
let lineSize = e.2
let path = NSBezierPath()
path.lineWidth = lineSize
path.lineCapStyle = .round
path.lineJoinStyle = .round
pathColor.set()
path.move(to: NSPoint(x: (points[0].x - minX!)*xCoff, y: (points[0].y - minY!)*yCoff))
for point in points {
let x = (point.x - minX!)*xCoff
let y = (point.y - minY!)*yCoff
path.line(to: NSPoint(x: x, y: y))
}
path.stroke()
}
}
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
let mainWindow: NSWindow? = NSWindow(contentRect: NSRect(x: 500, y: 300, width: 700, height: 500), styleMask: [.titled, .closable, .miniaturizable], backing: .buffered, defer: false)
var mainController: NSWindowController?
let frameView = DrawView(frame: NSRect(x: 70, y: 50, width: 560, height: 400))
func addPointSet(_ xSet: [CGFloat], _ ySet: [CGFloat], color: NSColor, size: CGFloat) {
frameView.addPointSet(xSet, ySet, color: color, size: size)
}
func applicationDidFinishLaunching(_ notification: Notification) {
mainWindow?.backgroundColor = .white
mainWindow?.orderFrontRegardless()
mainWindow?.contentView?.addSubview(frameView)
mainController = NSWindowController(window: mainWindow)
mainController?.showWindow(mainWindow)
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let delegate = AppDelegate()
@_cdecl("run")
public func run() {
let app = NSApplication.shared
app.delegate = delegate
app.activate(ignoringOtherApps: false)
app.setActivationPolicy(.regular)
app.run()
}
@_cdecl("add")
public func add(xSet: UnsafeMutablePointer<Double>, ySet: UnsafeMutablePointer<Double>, n: CInt, red: Double, green: Double, blue: Double, lineWidth: Double) {
var xs: [CGFloat] = []
var ys: [CGFloat] = []
for i in 0..<Int(n) {
xs.append(CGFloat(xSet[i]))
ys.append(CGFloat(ySet[i]))
}
let color = NSColor(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: 1)
let size = CGFloat(lineWidth)
delegate.addPointSet(xs, ys, color: color, size: size)
}
Và file main.c
:
#include <math.h>
void run();
void add(double*, double*, int, double, double, double, double);
int main() {
double x[50], y[50];
for (int i = 0; i < 50; i++) {
x[i] = i*5/49.0;
y[i] = exp(x[i]);
}
add(x, y, 50, 0, 0, 0, 1.5);
for (int i = 0; i < 50; i++)
y[i] = 2*x[i]*x[i] + 50;
add(x, y, 50, 0, 1, 0, 1.5);
for (int i = 0; i < 50; i++)
y[i] = 100 - 10*x[i];
add(x, y, 50, 1, 0, 0, 1.5);
run();
return 0;
}
Ta thu được kết quả:
Kết luận.
Kết hợp cùng bài viết trước, ta thấy rằng, ở thời điểm hiện tại, việc giao tiếp giữa C/C++
với Swift
là tương đối tốt. Do đó, chúng ta có thể linh hoạt sử dụng C/C++
hay Swift
tùy trường hợp để phát huy điểm mạnh của từng ngôn ngữ. Ví dụ, viết giao diện dùng các thư viện C/C++
thật sự phải gọi là đau đớn, nhưng về các cấu trúc dữ liệu, tính toán toán học thì C/C++
rất nhiều thư viện linh hoạt. Trong khi đó, Cocoa
thật sự khiến lập trình viên thoải mái khi viết ra các giao diện, tuy nhiên các thư viện tính toán toán học của Swift
rất hạn chế. Thay vào đó ta sử dụng C/C++
trong tính toán toán học và sử dụng Swift
để viết nên phần giao diện.
Bài viết có thể chưa hoàn thiện và không thể tránh sai sót, nhưng mong rằng bài viết nhỏ này giúp các bạn được ít nhiều.
All rights reserved