Skip to content

Commit 9a61873

Browse files
Merge pull request #4 from kolesa-team/get-bytes-fix
пробуем отловить проблему
2 parents 24eec5f + 40377b1 commit 9a61873

File tree

5 files changed

+116
-23
lines changed

5 files changed

+116
-23
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
language: go
2-
go: 1.13
2+
go: 1.18
33
go_import_path: https://github.com/kolesa-team/goexiv
44

55
cache:
@@ -22,7 +22,7 @@ before_install:
2222
test -d exiv2-${EXIV2_VERSION} && {
2323
cd exiv2-${EXIV2_VERSION}/build
2424
} || {
25-
wget http://www.exiv2.org/builds/exiv2-${EXIV2_VERSION}-Source.tar.gz
25+
wget https://github.com/Exiv2/exiv2/releases/download/v${EXIV2_VERSION}/exiv2-${EXIV2_VERSION}-Source.tar.gz
2626
tar xzf exiv2-${EXIV2_VERSION}-Source.tar.gz
2727
mv exiv2-${EXIV2_VERSION}-Source exiv2-${EXIV2_VERSION}
2828
cd exiv2-${EXIV2_VERSION}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ if err != nil {
7171

7272
fmt.Println(userComment)
7373
// "A comment. Might be a JSON string. Можно писать и по-русски!"
74+
75+
// It is advisable to insert this line at the end of `goexivImg` lifecycle.
76+
// see exiv_test.go:Test_GetBytes_Goroutine
77+
runtime.KeepAlive(goexivImg)
7478
```
7579
7680
Changing the image metadata in memory and returning the updated image (an approach fit for a web service):

exiv.go

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import "C"
77

88
import (
99
"errors"
10-
"reflect"
1110
"runtime"
1211
"unsafe"
1312
)
@@ -18,7 +17,8 @@ type Error struct {
1817
}
1918

2019
type Image struct {
21-
img *C.Exiv2Image
20+
bytesArrayPtr unsafe.Pointer
21+
img *C.Exiv2Image
2222
}
2323

2424
type MetadataProvider interface {
@@ -42,13 +42,18 @@ func makeError(cerr *C.Exiv2Error) *Error {
4242
}
4343
}
4444

45-
func makeImage(cimg *C.Exiv2Image) *Image {
45+
func makeImage(cimg *C.Exiv2Image, bytesPtr unsafe.Pointer) *Image {
4646
img := &Image{
47-
cimg,
47+
bytesArrayPtr: bytesPtr,
48+
img: cimg,
4849
}
4950

5051
runtime.SetFinalizer(img, func(x *Image) {
5152
C.exiv2_image_free(x.img)
53+
54+
if x.bytesArrayPtr != nil {
55+
C.free(x.bytesArrayPtr)
56+
}
5257
})
5358

5459
return img
@@ -71,26 +76,33 @@ func Open(path string) (*Image, error) {
7176
return nil, err
7277
}
7378

74-
return makeImage(cimg), nil
79+
return makeImage(cimg, nil), nil
7580
}
7681

7782
// OpenBytes opens a byte slice with image data and returns a pointer to
7883
// the corresponding Image object, but does not read the Metadata.
7984
// Start the parsing with a call to ReadMetadata()
80-
func OpenBytes(b []byte) (*Image, error) {
81-
if len(b) == 0 {
85+
func OpenBytes(input []byte) (*Image, error) {
86+
if len(input) == 0 {
8287
return nil, &Error{0, "input is empty"}
8388
}
89+
8490
var cerr *C.Exiv2Error
85-
cimg := C.exiv2_image_factory_open_bytes((*C.uchar)(unsafe.Pointer(&b[0])), C.long(len(b)), &cerr)
91+
92+
bytesArrayPtr := C.CBytes(input)
93+
cimg := C.exiv2_image_factory_open_bytes(
94+
(*C.uchar)(bytesArrayPtr),
95+
C.long(len(input)),
96+
&cerr,
97+
)
8698

8799
if cerr != nil {
88100
err := makeError(cerr)
89101
C.exiv2_error_free(cerr)
90102
return nil, err
91103
}
92104

93-
return makeImage(cimg), nil
105+
return makeImage(cimg, bytesArrayPtr), nil
94106
}
95107

96108
// ReadMetadata reads the metadata of an Image
@@ -111,19 +123,10 @@ func (i *Image) ReadMetadata() error {
111123
// Returns an image contents.
112124
// If its metadata has been changed, the changes are reflected here.
113125
func (i *Image) GetBytes() []byte {
114-
size := int(C.exiv_image_get_size(i.img))
126+
size := C.exiv_image_get_size(i.img)
115127
ptr := C.exiv_image_get_bytes_ptr(i.img)
116128

117-
var slice []byte
118-
header := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
119-
header.Cap = size
120-
header.Len = size
121-
header.Data = uintptr(unsafe.Pointer(ptr))
122-
123-
target := make([]byte, len(slice))
124-
copy(target, slice)
125-
126-
return target
129+
return C.GoBytes(unsafe.Pointer(ptr), C.int(size))
127130
}
128131

129132
// PixelWidth returns the width of the image in pixels

exiv_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"github.com/stretchr/testify/assert"
66
"github.com/stretchr/testify/require"
77
"io/ioutil"
8+
"runtime"
9+
"sync"
810
"testing"
911
)
1012

@@ -407,6 +409,90 @@ func Test_GetBytes(t *testing.T) {
407409
)
408410
}
409411

412+
// Ensures image manipulation doesn't fail when running from multiple goroutines
413+
func Test_GetBytes_Goroutine(t *testing.T) {
414+
var wg sync.WaitGroup
415+
iterations := 0
416+
417+
bytes, err := ioutil.ReadFile("testdata/stripped_pixel.jpg")
418+
require.NoError(t, err)
419+
420+
for i := 0; i < 100; i++ {
421+
iterations++
422+
wg.Add(1)
423+
424+
go func(i int) {
425+
defer wg.Done()
426+
427+
img, err := goexiv.OpenBytes(bytes)
428+
require.NoError(t, err)
429+
430+
// trigger garbage collection to increase the chance that underlying img.img will be collected
431+
runtime.GC()
432+
433+
bytesAfter := img.GetBytes()
434+
assert.NotEmpty(t, bytesAfter)
435+
436+
// if this line is removed, then the test will likely fail
437+
// with segmentation violation.
438+
// so far we couldn't come up with a better solution.
439+
runtime.KeepAlive(img)
440+
}(i)
441+
}
442+
443+
wg.Wait()
444+
runtime.GC()
445+
var memStats runtime.MemStats
446+
runtime.ReadMemStats(&memStats)
447+
t.Logf("Allocated bytes after test: %+v\n", memStats.HeapAlloc)
448+
}
449+
450+
func BenchmarkImage_GetBytes_KeepAlive(b *testing.B) {
451+
bytes, err := ioutil.ReadFile("testdata/stripped_pixel.jpg")
452+
require.NoError(b, err)
453+
var wg sync.WaitGroup
454+
455+
for i := 0; i < b.N; i++ {
456+
wg.Add(1)
457+
go func() {
458+
defer wg.Done()
459+
460+
img, err := goexiv.OpenBytes(bytes)
461+
require.NoError(b, err)
462+
463+
runtime.GC()
464+
465+
require.NoError(b, img.SetExifString("Exif.Photo.UserComment", "123"))
466+
467+
bytesAfter := img.GetBytes()
468+
assert.NotEmpty(b, bytesAfter)
469+
runtime.KeepAlive(img)
470+
}()
471+
}
472+
473+
wg.Wait()
474+
}
475+
476+
func BenchmarkImage_GetBytes_NoKeepAlive(b *testing.B) {
477+
bytes, err := ioutil.ReadFile("testdata/stripped_pixel.jpg")
478+
require.NoError(b, err)
479+
var wg sync.WaitGroup
480+
481+
for i := 0; i < b.N; i++ {
482+
wg.Add(1)
483+
go func() {
484+
defer wg.Done()
485+
img, err := goexiv.OpenBytes(bytes)
486+
require.NoError(b, err)
487+
488+
require.NoError(b, img.SetExifString("Exif.Photo.UserComment", "123"))
489+
490+
bytesAfter := img.GetBytes()
491+
assert.NotEmpty(b, bytesAfter)
492+
}()
493+
}
494+
}
495+
410496
// Fills the image with metadata
411497
func initializeImage(path string, t *testing.T) {
412498
img, err := goexiv.Open(path)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ require (
66
github.com/stretchr/testify v1.2.2
77
)
88

9-
go 1.13
9+
go 1.18

0 commit comments

Comments
 (0)