Skip to content

Commit 6427cec

Browse files
committed
fix: address oracle code review findings
- Fix CString memory leaks in New, NewBuilder, AddWord, LoadDict - Add error returns to New and NewBuilder (breaking API change) - Add thread safety documentation to Kiwi type - Add test coverage: New failure, WithOpenEnding, WithAllowedDialects - Fix error handling in all tests (assert.NoError) - Apply gofumpt formatting
1 parent 6c2ec5e commit 6427cec

3 files changed

Lines changed: 107 additions & 28 deletions

File tree

kiwi.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -292,14 +292,18 @@ func KiwiClearError() {
292292
}
293293

294294
// Kiwi is a wrapper for the kiwi C library.
295+
//
296+
// Thread Safety: Concurrent calls to Analyze are safe.
297+
// However, SetGlobalConfig must not be called concurrently with Analyze
298+
// or other methods that read the configuration.
295299
type Kiwi struct {
296300
handler C.kiwi_h
297301
dialects Dialect
298302
}
299303

300304
// New returns a new Kiwi instance.
301305
// Don't forget to call Close after this.
302-
func New(modelPath string, opts ...Option) *Kiwi {
306+
func New(modelPath string, opts ...Option) (*Kiwi, error) {
303307
options := kiwiOptions{
304308
buildOptions: KIWI_BUILD_DEFAULT,
305309
dialects: DialectStandard,
@@ -309,10 +313,18 @@ func New(modelPath string, opts ...Option) *Kiwi {
309313
opt(&options)
310314
}
311315

316+
cModelPath := C.CString(modelPath)
317+
defer C.free(unsafe.Pointer(cModelPath))
318+
319+
h := C.kiwi_init(cModelPath, C.int(options.numThread), C.int(options.buildOptions), C.int(options.dialects))
320+
if h == nil {
321+
return nil, fmt.Errorf("kiwi_init failed: %s", KiwiError())
322+
}
323+
312324
return &Kiwi{
313-
handler: C.kiwi_init(C.CString(modelPath), C.int(options.numThread), C.int(options.buildOptions), C.int(options.dialects)),
325+
handler: h,
314326
dialects: options.dialects,
315-
}
327+
}, nil
316328
}
317329

318330
// TokenInfo returns the token info for the given token(Str).
@@ -476,7 +488,7 @@ type KiwiBuilder struct {
476488

477489
// NewBuilder returns a new KiwiBuilder instance.
478490
// Don't forget to call Close after this.
479-
func NewBuilder(modelPath string, opts ...Option) *KiwiBuilder {
491+
func NewBuilder(modelPath string, opts ...Option) (*KiwiBuilder, error) {
480492
options := kiwiOptions{
481493
buildOptions: KIWI_BUILD_DEFAULT,
482494
dialects: DialectStandard,
@@ -486,19 +498,36 @@ func NewBuilder(modelPath string, opts ...Option) *KiwiBuilder {
486498
opt(&options)
487499
}
488500

489-
return &KiwiBuilder{
490-
handler: C.kiwi_builder_init(C.CString(modelPath), C.int(options.numThread), C.int(options.buildOptions), C.int(options.dialects)),
501+
cModelPath := C.CString(modelPath)
502+
defer C.free(unsafe.Pointer(cModelPath))
503+
504+
h := C.kiwi_builder_init(cModelPath, C.int(options.numThread), C.int(options.buildOptions), C.int(options.dialects))
505+
if h == nil {
506+
return nil, fmt.Errorf("kiwi_builder_init failed: %s", KiwiError())
491507
}
508+
509+
return &KiwiBuilder{
510+
handler: h,
511+
}, nil
492512
}
493513

494514
// AddWord set custom word with word, pos, score.
495515
func (kb *KiwiBuilder) AddWord(word string, pos POSType, score float32) int {
496-
return int(C.kiwi_builder_add_word(kb.handler, C.CString(word), C.CString(string(pos)), C.float(score)))
516+
cWord := C.CString(word)
517+
defer C.free(unsafe.Pointer(cWord))
518+
519+
cPos := C.CString(string(pos))
520+
defer C.free(unsafe.Pointer(cPos))
521+
522+
return int(C.kiwi_builder_add_word(kb.handler, cWord, cPos, C.float(score)))
497523
}
498524

499525
// LoadDict loads user dict with dict file path.
500526
func (kb *KiwiBuilder) LoadDict(dictPath string) int {
501-
return int(C.kiwi_builder_load_dict(kb.handler, C.CString(dictPath)))
527+
cDictPath := C.CString(dictPath)
528+
defer C.free(unsafe.Pointer(cDictPath))
529+
530+
return int(C.kiwi_builder_load_dict(kb.handler, cDictPath))
502531
}
503532

504533
// Build creates kiwi instance with user word etc.

kiwi_example_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import (
77
)
88

99
func Example() {
10-
kb := kiwi.NewBuilder("./base", kiwi.WithNumThread(1), kiwi.WithBuildOption(kiwi.KIWI_BUILD_INTEGRATE_ALLOMORPH))
10+
kb, err := kiwi.NewBuilder("./base", kiwi.WithNumThread(1), kiwi.WithBuildOption(kiwi.KIWI_BUILD_INTEGRATE_ALLOMORPH))
11+
if err != nil {
12+
panic(err)
13+
}
1114
kb.AddWord("코딩냄비", "NNP", 0)
1215

1316
k := kb.Build()

kiwi_test.go

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ func TestKiwiVersion(t *testing.T) {
2020
}
2121

2222
func TestAnalyze(t *testing.T) {
23-
kiwi := New("./base", WithNumThread(1))
24-
res, _ := kiwi.Analyze("아버지가 방에 들어가신다")
23+
kiwi, err := New("./base", WithNumThread(1))
24+
assert.NoError(t, err)
25+
res, err := kiwi.Analyze("아버지가 방에 들어가신다")
26+
assert.NoError(t, err)
2527

2628
expected := []TokenResult{
2729
{
@@ -73,8 +75,10 @@ func TestAnalyze(t *testing.T) {
7375
}
7476

7577
func TestSplitSentence(t *testing.T) {
76-
kiwi := New("./base", WithNumThread(1))
77-
res, _ := kiwi.SplitSentence("여러 문장으로 구성된 텍스트네 이걸 분리해줘", KIWI_MATCH_ALL)
78+
kiwi, err := New("./base", WithNumThread(1))
79+
assert.NoError(t, err)
80+
res, err := kiwi.SplitSentence("여러 문장으로 구성된 텍스트네 이걸 분리해줘", KIWI_MATCH_ALL)
81+
assert.NoError(t, err)
7882

7983
expected := []SplitResult{
8084
{
@@ -94,7 +98,8 @@ func TestSplitSentence(t *testing.T) {
9498
}
9599

96100
func TestAddWordFail(t *testing.T) {
97-
kb := NewBuilder("./base", WithNumThread(1), WithBuildOption(KIWI_BUILD_INTEGRATE_ALLOMORPH))
101+
kb, err := NewBuilder("./base", WithNumThread(1), WithBuildOption(KIWI_BUILD_INTEGRATE_ALLOMORPH))
102+
assert.NoError(t, err)
98103
add := kb.AddWord("아버지가", "SKO", 0)
99104
assert.Equal(t, -1, add)
100105
assert.Equal(t, 0, kb.Close())
@@ -103,7 +108,8 @@ func TestAddWordFail(t *testing.T) {
103108
}
104109

105110
func TestAddWord(t *testing.T) {
106-
kb := NewBuilder("./base", WithNumThread(1), WithBuildOption(KIWI_BUILD_INTEGRATE_ALLOMORPH))
111+
kb, err := NewBuilder("./base", WithNumThread(1), WithBuildOption(KIWI_BUILD_INTEGRATE_ALLOMORPH))
112+
assert.NoError(t, err)
107113
add := kb.AddWord("아버지가", "NNG", 0)
108114

109115
assert.Equal(t, 0, add)
@@ -159,14 +165,15 @@ func TestAddWord(t *testing.T) {
159165
}
160166

161167
func TestLoadDict(t *testing.T) {
162-
kb := NewBuilder("./base", WithNumThread(1), WithBuildOption(KIWI_BUILD_INTEGRATE_ALLOMORPH))
168+
kb, err := NewBuilder("./base", WithNumThread(1), WithBuildOption(KIWI_BUILD_INTEGRATE_ALLOMORPH))
169+
assert.NoError(t, err)
163170
add := kb.LoadDict("./example/user_dict.tsv")
164171

165172
assert.Equal(t, 1, add)
166173

167-
err := KiwiError()
174+
errMsg := KiwiError()
168175

169-
assert.Equal(t, "", err)
176+
assert.Equal(t, "", errMsg)
170177

171178
kiwi := kb.Build()
172179

@@ -220,14 +227,15 @@ func TestLoadDict(t *testing.T) {
220227
}
221228

222229
func TestLoadDict2(t *testing.T) {
223-
kb := NewBuilder("./base", WithNumThread(1), WithBuildOption(KIWI_BUILD_INTEGRATE_ALLOMORPH))
230+
kb, err := NewBuilder("./base", WithNumThread(1), WithBuildOption(KIWI_BUILD_INTEGRATE_ALLOMORPH))
231+
assert.NoError(t, err)
224232
add := kb.LoadDict("./example/user_dict2.tsv")
225233

226234
assert.Equal(t, 3, add)
227235

228-
err := KiwiError()
236+
errMsg := KiwiError()
229237

230-
assert.Equal(t, "", err)
238+
assert.Equal(t, "", errMsg)
231239

232240
kiwi := kb.Build()
233241
res, _ := kiwi.Analyze("아버지가 방에 들어가신다")
@@ -262,7 +270,8 @@ func TestLoadDict2(t *testing.T) {
262270
}
263271

264272
func TestExtractWord(t *testing.T) {
265-
kb := NewBuilder("./base", WithNumThread(1))
273+
kb, err := NewBuilder("./base", WithNumThread(1))
274+
assert.NoError(t, err)
266275
rs := strings.NewReader(`2008년에는 애국가의 작곡자 안익태가 1930년대에 독일 유학 기간 중 친일 활동을 했다는 사실이 밝혀졌다. 이후 안익태가 나치 독일 하의
267276
베를린에서 만주국 10주년 건국 기념음악회를 지휘하는 동영상까지 발굴되어 관련 학계나 사회에 큰 충격을 주었다. 안익태가 친일 행적을 한 바
268277
있다는 빼도박도 못할 증거가 나왔으니까. 영상물의 '만주환상곡'에는 우리가 현재 알고있는 '한국환상곡'의 두 선율("무궁화 삼천리 나의 사랑아,
@@ -294,7 +303,8 @@ func TestExtractWord(t *testing.T) {
294303
}
295304

296305
func TestExtractWordwithFile(t *testing.T) {
297-
kb := NewBuilder("./base", WithNumThread(1)) // Use single thread for deterministic results
306+
kb, err := NewBuilder("./base", WithNumThread(1)) // Use single thread for deterministic results
307+
assert.NoError(t, err)
298308
file, _ := os.Open("./example/test.txt")
299309

300310
wordInfos, _ := kb.ExtractWords(file, 10 /*=minCnt*/, 5 /*=maxWordLen*/, 0.0 /*=minScore*/, -25.0 /*=posThreshold*/)
@@ -308,7 +318,8 @@ func TestExtractWordwithFile(t *testing.T) {
308318
}
309319

310320
func TestGetGlobalConfig(t *testing.T) {
311-
kiwi := New("./base", WithNumThread(1))
321+
kiwi, err := New("./base", WithNumThread(1))
322+
assert.NoError(t, err)
312323
defer kiwi.Close()
313324

314325
config := kiwi.GetGlobalConfig()
@@ -319,7 +330,8 @@ func TestGetGlobalConfig(t *testing.T) {
319330
}
320331

321332
func TestSetGlobalConfig(t *testing.T) {
322-
kiwi := New("./base", WithNumThread(1))
333+
kiwi, err := New("./base", WithNumThread(1))
334+
assert.NoError(t, err)
323335
defer kiwi.Close()
324336

325337
originalConfig := kiwi.GetGlobalConfig()
@@ -335,15 +347,17 @@ func TestSetGlobalConfig(t *testing.T) {
335347
}
336348

337349
func TestGetSetOption(t *testing.T) {
338-
kiwi := New("./base", WithNumThread(1))
350+
kiwi, err := New("./base", WithNumThread(1))
351+
assert.NoError(t, err)
339352
defer kiwi.Close()
340353

341354
threads := kiwi.GetOption(KIWI_NUM_THREADS)
342355
assert.True(t, threads >= 1)
343356
}
344357

345358
func TestMatchOptionOOV(t *testing.T) {
346-
kiwi := New("./base", WithNumThread(1))
359+
kiwi, err := New("./base", WithNumThread(1))
360+
assert.NoError(t, err)
347361
defer kiwi.Close()
348362

349363
// Use OOV-containing text to detect differences between OOV modes
@@ -361,7 +375,8 @@ func TestMatchOptionOOV(t *testing.T) {
361375
}
362376

363377
func TestMorphset(t *testing.T) {
364-
kiwi := New("./base", WithNumThread(1))
378+
kiwi, err := New("./base", WithNumThread(1))
379+
assert.NoError(t, err)
365380
defer kiwi.Close()
366381

367382
ms, err := kiwi.NewMorphset()
@@ -383,3 +398,35 @@ func TestMorphset(t *testing.T) {
383398
assert.NotEqual(t, "아버지", token.Form)
384399
}
385400
}
401+
402+
func TestNewFailure(t *testing.T) {
403+
_, err := New("./nonexistent_path")
404+
assert.Error(t, err)
405+
assert.Contains(t, err.Error(), "kiwi_init failed")
406+
}
407+
408+
func TestWithOpenEnding(t *testing.T) {
409+
kiwi, err := New("./base", WithNumThread(1))
410+
assert.NoError(t, err)
411+
defer kiwi.Close()
412+
413+
res, err := kiwi.Analyze("아버지가 방에 들어가신다", WithOpenEnding(true))
414+
assert.NoError(t, err)
415+
assert.True(t, len(res) > 0)
416+
}
417+
418+
func TestWithAllowedDialects(t *testing.T) {
419+
kiwi, err := New("./base", WithNumThread(1), WithDialect(DialectGyeongsang))
420+
assert.NoError(t, err)
421+
defer kiwi.Close()
422+
423+
// Test with default dialects (should use instance setting)
424+
res, err := kiwi.Analyze("아버지가 방에 들어가신다")
425+
assert.NoError(t, err)
426+
assert.True(t, len(res) > 0)
427+
428+
// Test with explicit dialect override
429+
res, err = kiwi.Analyze("아버지가 방에 들어가신다", WithAllowedDialects(DialectStandard))
430+
assert.NoError(t, err)
431+
assert.True(t, len(res) > 0)
432+
}

0 commit comments

Comments
 (0)