diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 00000000..4c510ca7 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,58 @@ +name: Go + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + go-test: + name: Go Test - ${{ matrix.platform }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: windows-latest + target: x86_64-pc-windows-gnu + platform: windows-amd64 + lib_name: libbraillify_go.a + - runner: ubuntu-latest + target: x86_64-unknown-linux-gnu + platform: linux-amd64 + lib_name: libbraillify_go.a + - runner: macos-14 + target: aarch64-apple-darwin + platform: darwin-arm64 + lib_name: libbraillify_go.a + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.21" + + - name: Build native library + run: cargo build --release --target ${{ matrix.target }} -p braillify-go + + - name: Copy native library + run: | + mkdir -p packages/go/libs/${{ matrix.platform }} + cp target/${{ matrix.target }}/release/${{ matrix.lib_name }} packages/go/libs/${{ matrix.platform }}/ + shell: bash + + - name: Test + run: go test -v ./... + working-directory: packages/go diff --git a/Cargo.lock b/Cargo.lock index 8f27dc08..840fdf3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,6 +139,13 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "braillify-go" +version = "2.0.0" +dependencies = [ + "braillify", +] + [[package]] name = "bstr" version = "1.12.1" diff --git a/packages/go/.gitignore b/packages/go/.gitignore new file mode 100644 index 00000000..ce3cdc42 --- /dev/null +++ b/packages/go/.gitignore @@ -0,0 +1,3 @@ +*.test +*.exe +*.out diff --git a/packages/go/Cargo.toml b/packages/go/Cargo.toml new file mode 100644 index 00000000..1a17dae8 --- /dev/null +++ b/packages/go/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "braillify-go" +version = "2.0.0" +edition = "2024" + +[lib] +name = "braillify_go" +crate-type = ["cdylib", "staticlib"] + +[dependencies] +braillify = { path = "../../libs/braillify", default-features = false } diff --git a/packages/go/braillify.go b/packages/go/braillify.go new file mode 100644 index 00000000..597e1d25 --- /dev/null +++ b/packages/go/braillify.go @@ -0,0 +1,16 @@ +package braillify + +// Encode converts Korean text to braille byte representation. +func Encode(text string) ([]byte, error) { + return cEncode(text) +} + +// EncodeToUnicode converts Korean text to braille Unicode string. +func EncodeToUnicode(text string) (string, error) { + return cEncodeToUnicode(text) +} + +// EncodeToBrailleFont converts Korean text to braille font string. +func EncodeToBrailleFont(text string) (string, error) { + return cEncodeToBrailleFont(text) +} diff --git a/packages/go/braillify_test.go b/packages/go/braillify_test.go new file mode 100644 index 00000000..e12bdfab --- /dev/null +++ b/packages/go/braillify_test.go @@ -0,0 +1,51 @@ +package braillify + +import "testing" + +func TestEncodeToUnicode(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"안녕하세요", "⠣⠒⠉⠻⠚⠠⠝⠬"}, + {"상상이상의", "⠇⠶⠇⠶⠕⠇⠶⠺"}, + {"1,000", "⠼⠁⠂⠚⠚⠚"}, + {"ATM", "⠠⠠⠁⠞⠍"}, + {"", ""}, + } + + for _, tt := range tests { + result, err := EncodeToUnicode(tt.input) + if err != nil { + t.Errorf("EncodeToUnicode(%q): unexpected error: %v", tt.input, err) + continue + } + t.Logf("EncodeToUnicode(%q) = %q", tt.input, result) + if result != tt.expected { + t.Errorf("EncodeToUnicode(%q) = %q, want %q", tt.input, result, tt.expected) + } + } +} + +func TestEncode(t *testing.T) { + result, err := Encode("안녕") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + t.Logf("Encode(%q) = %v", "안녕", result) + if len(result) == 0 { + t.Error("expected non-empty byte slice") + } +} + +func TestEncodeToBrailleFont(t *testing.T) { + result, err := EncodeToBrailleFont("안녕하세요") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := "⠣⠒⠉⠻⠚⠠⠝⠬" + t.Logf("EncodeToBrailleFont(%q) = %q", "안녕하세요", result) + if result != expected { + t.Errorf("EncodeToBrailleFont = %q, want %q", result, expected) + } +} diff --git a/packages/go/cgo.go b/packages/go/cgo.go new file mode 100644 index 00000000..3048345d --- /dev/null +++ b/packages/go/cgo.go @@ -0,0 +1,85 @@ +package braillify + +/* +#cgo darwin,amd64 LDFLAGS: -L${SRCDIR}/libs/darwin-amd64 -lbraillify_go -lm -lpthread +#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/libs/darwin-arm64 -lbraillify_go -lm -lpthread +#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/libs/linux-amd64 -lbraillify_go -lm -lpthread -ldl +#cgo linux,arm64 LDFLAGS: -L${SRCDIR}/libs/linux-arm64 -lbraillify_go -lm -lpthread -ldl +#cgo windows,amd64 LDFLAGS: -L${SRCDIR}/libs/windows-amd64 -lbraillify_go -lntdll -lws2_32 -lbcrypt -ladvapi32 -luserenv + +#include +#include +#include + +extern uint8_t* braillify_encode(const char* text, size_t* out_len); +extern char* braillify_encode_to_unicode(const char* text); +extern char* braillify_encode_to_braille_font(const char* text); +extern char* braillify_get_last_error(); +extern void braillify_free_string(char* ptr); +extern void braillify_free_bytes(uint8_t* ptr, size_t len); +*/ +import "C" + +import ( + "errors" + "runtime" + "unsafe" +) + +func cEncode(text string) ([]byte, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + cText := C.CString(text) + defer C.free(unsafe.Pointer(cText)) + + var outLen C.size_t + result := C.braillify_encode(cText, &outLen) + if result == nil { + return nil, getLastError() + } + defer C.braillify_free_bytes(result, outLen) + + return C.GoBytes(unsafe.Pointer(result), C.int(outLen)), nil +} + +func cEncodeToUnicode(text string) (string, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + cText := C.CString(text) + defer C.free(unsafe.Pointer(cText)) + + result := C.braillify_encode_to_unicode(cText) + if result == nil { + return "", getLastError() + } + defer C.braillify_free_string(result) + + return C.GoString(result), nil +} + +func cEncodeToBrailleFont(text string) (string, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + cText := C.CString(text) + defer C.free(unsafe.Pointer(cText)) + + result := C.braillify_encode_to_braille_font(cText) + if result == nil { + return "", getLastError() + } + defer C.braillify_free_string(result) + + return C.GoString(result), nil +} + +func getLastError() error { + errPtr := C.braillify_get_last_error() + if errPtr == nil { + return errors.New("braillify: unknown error") + } + defer C.braillify_free_string(errPtr) + return errors.New(C.GoString(errPtr)) +} diff --git a/packages/go/go.mod b/packages/go/go.mod new file mode 100644 index 00000000..d54c11dd --- /dev/null +++ b/packages/go/go.mod @@ -0,0 +1,3 @@ +module github.com/dev-five-git/braillify/packages/go + +go 1.21 diff --git a/packages/go/libs/windows-amd64/libbraillify_go.a b/packages/go/libs/windows-amd64/libbraillify_go.a new file mode 100644 index 00000000..cb7fe9de Binary files /dev/null and b/packages/go/libs/windows-amd64/libbraillify_go.a differ diff --git a/packages/go/src/lib.rs b/packages/go/src/lib.rs new file mode 100644 index 00000000..31ced796 --- /dev/null +++ b/packages/go/src/lib.rs @@ -0,0 +1,145 @@ +use std::cell::RefCell; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::ptr; + +thread_local! { + static LAST_ERROR: RefCell> = const { RefCell::new(None) }; +} + +fn set_last_error(err: String) { + LAST_ERROR.with(|e| { + *e.borrow_mut() = Some(err); + }); +} + +fn clear_last_error() { + LAST_ERROR.with(|e| { + *e.borrow_mut() = None; + }); +} + +#[unsafe(no_mangle)] +pub extern "C" fn braillify_get_last_error() -> *mut c_char { + LAST_ERROR.with(|e| match e.borrow().as_ref() { + Some(msg) => CString::new(msg.clone()) + .map(|s| s.into_raw()) + .unwrap_or(ptr::null_mut()), + None => ptr::null_mut(), + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_encode(text: *const c_char, out_len: *mut usize) -> *mut u8 { + clear_last_error(); + + if text.is_null() || out_len.is_null() { + set_last_error("Null pointer argument".to_string()); + return ptr::null_mut(); + } + + let c_str = unsafe { CStr::from_ptr(text) }; + let text_str = match c_str.to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(format!("Invalid UTF-8: {}", e)); + return ptr::null_mut(); + } + }; + + match braillify::encode(text_str) { + Ok(result) => { + unsafe { *out_len = result.len() }; + let boxed = result.into_boxed_slice(); + Box::into_raw(boxed) as *mut u8 + } + Err(e) => { + set_last_error(e); + ptr::null_mut() + } + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_encode_to_unicode(text: *const c_char) -> *mut c_char { + clear_last_error(); + + if text.is_null() { + set_last_error("Null pointer argument".to_string()); + return ptr::null_mut(); + } + + let c_str = unsafe { CStr::from_ptr(text) }; + let text_str = match c_str.to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(format!("Invalid UTF-8: {}", e)); + return ptr::null_mut(); + } + }; + + match braillify::encode_to_unicode(text_str) { + Ok(result) => match CString::new(result) { + Ok(c_string) => c_string.into_raw(), + Err(e) => { + set_last_error(format!("CString conversion error: {}", e)); + ptr::null_mut() + } + }, + Err(e) => { + set_last_error(e); + ptr::null_mut() + } + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_encode_to_braille_font(text: *const c_char) -> *mut c_char { + clear_last_error(); + + if text.is_null() { + set_last_error("Null pointer argument".to_string()); + return ptr::null_mut(); + } + + let c_str = unsafe { CStr::from_ptr(text) }; + let text_str = match c_str.to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(format!("Invalid UTF-8: {}", e)); + return ptr::null_mut(); + } + }; + + match braillify::encode_to_braille_font(text_str) { + Ok(result) => match CString::new(result) { + Ok(c_string) => c_string.into_raw(), + Err(e) => { + set_last_error(format!("CString conversion error: {}", e)); + ptr::null_mut() + } + }, + Err(e) => { + set_last_error(e); + ptr::null_mut() + } + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_free_string(ptr: *mut c_char) { + if !ptr.is_null() { + unsafe { + drop(CString::from_raw(ptr)); + } + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_free_bytes(ptr: *mut u8, len: usize) { + if !ptr.is_null() { + unsafe { + let _ = Vec::from_raw_parts(ptr, len, len); + } + } +}