everscale-client-ruby 1.1.23

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,339 @@
1
+ require 'byebug'
2
+ require 'json'
3
+ require File.dirname(__FILE__) + '/helpers.rb'
4
+
5
+ class CodeGenerator
6
+ include InstanceHelpers
7
+
8
+ attr_accessor :types
9
+ attr_accessor :root_dir
10
+
11
+ TAB = " "
12
+
13
+ def initialize(types, root_dir)
14
+ @types = types
15
+ @root_dir = root_dir
16
+ end
17
+
18
+ def generate_self_code
19
+ generateModules(types)
20
+ generateReadme(types)
21
+ end
22
+
23
+ private
24
+
25
+ def generateModules(types)
26
+ types.modules.each do |mod|
27
+ modulesFolder = root_dir + "/lib/everscale-client-ruby/Client"
28
+ moduleFilePath = "#{modulesFolder}/#{mod.name.capitalize}.rb"
29
+ newModuleContent = ""
30
+ if mod.name == "client"
31
+ newModuleContent = generateClientModule(mod, types.modules)
32
+ else
33
+ newModuleContent = generateModule(mod)
34
+ end
35
+ if File.exists?(moduleFilePath)
36
+ File.delete(moduleFilePath)
37
+ end
38
+ File.open(moduleFilePath, 'w+') { |f| f.write(newModuleContent) }
39
+ end
40
+
41
+ p 'generate_self_code ok'
42
+ end
43
+
44
+ def generateReadme(types)
45
+ readmePath = root_dir + "/README.md"
46
+ content = %{
47
+ # Ruby Client for Free TON SDK
48
+
49
+ [![GEM](https://img.shields.io/badge/ruby-gem-orange)](https://rubygems.org/gems/everscale-client-ruby)
50
+ [![SPM](https://img.shields.io/badge/SDK%20VERSION-#{types.version}-green)](https://github.com/tonlabs/TON-SDK)
51
+
52
+ ## Install
53
+
54
+ Install gem
55
+ ```bash
56
+ gem install ton-client-ruby
57
+ ```
58
+
59
+ Install TON-SDK
60
+ ```bash
61
+ ton-client-ruby setup
62
+ # result - path to dylib file for ton-client-ruby configuration
63
+ ```
64
+
65
+ ### Manual build FreeTON SDK
66
+ 0. Install Rust to your OS
67
+ 1. git clone https://github.com/tonlabs/TON-SDK
68
+ 2. cd ./TON-SDK
69
+ 3. cargo update
70
+ 4. cargo build --release
71
+
72
+ ## Usage
73
+
74
+ ```ruby
75
+ # For MAcOS
76
+ TonClient.configure { |config| config.ffi_lib(./TON-SDK/target/release/libton_client.dylib) }
77
+ # For Linux
78
+ # TonClient.configure { |config| config.ffi_lib(./TON-SDK/target/release/libton_client.so) }
79
+
80
+ client = TonClient.create(config: {network: {server_address: "net.ton.dev"}})
81
+
82
+ # All methods are asynchronous
83
+
84
+ # example: call method for Crypto module
85
+ payload = {composite: '17ED48941A08F981'}
86
+ client.crypto.factorize(payload) do |response|
87
+ p response.result['factors']
88
+ end
89
+
90
+ # e.g. ...
91
+ ```\n\n
92
+ }
93
+ content << "## All Modules, methods and types\n\n"
94
+
95
+ # types
96
+ content << "<details>\n#{TAB}<summary>Types</summary>\n\n"
97
+
98
+ content << "#{customTypes()}\n"
99
+
100
+ types.modules.each do |mod|
101
+ (mod.enums || []).each do |type|
102
+ content << checkComment(type.summary, 0) if type.summary
103
+ content << checkComment(type.description, 0) if type.description
104
+ content << "\n- #### #{type.name}\n"
105
+ (type.cases || []).each do |enum_case|
106
+ content << "#{checkComment(enum_case.summary, 1).gsub(/#/, '')}" if enum_case.summary
107
+ content << "#{checkComment(enum_case.description, 1).gsub(/#/, '')}" if enum_case.description
108
+ content << "#{TAB}- case #{enum_case.name} = #{enum_case.value}\n\n"
109
+ end
110
+ end
111
+
112
+ (mod.types || []).each do |type|
113
+ content << checkComment(type.summary, 0) if type.summary
114
+ content << checkComment(type.description, 0) if type.description
115
+ content << "\n- #### #{type.name}\n"
116
+ (type.fields || []).each do |field|
117
+ content << "#{checkComment(field.summary, 1).gsub(/#/, '')}\n" if field.summary
118
+ content << "#{checkComment(field.description, 1).gsub(/#/, '')}\n" if field.description
119
+ content << "#{TAB}- #{field.name}: #{field.type}\n\n"
120
+ end
121
+ end
122
+ end
123
+ content << "</details>\n\n"
124
+
125
+ # methods
126
+ types.modules.each do |mod|
127
+ content << "<details>\n#{TAB}<summary>#{mod.name&.upcase}</summary>\n\n"
128
+ (mod.functions || []).each do |function|
129
+ content << "```ruby\n"
130
+ content << checkComment(function.summary, 2) if function.summary
131
+ content << checkComment(function.description, 2) if function.description
132
+ content << "\n#{TAB}#{TAB}def #{function.name}"
133
+ if function.arguments.empty?
134
+ content << "(&block)\n"
135
+ else
136
+ content << "(payload, &block)\n"
137
+ end
138
+ content << getFunctionComment(function, types)
139
+ content << "```\n"
140
+ end
141
+ content << "</details>\n\n"
142
+ end
143
+
144
+ content << %{
145
+ \n## Tests
146
+
147
+ 1. create __.env.test__ file inside root directory of this library with variables
148
+
149
+ example for NodeSE
150
+ ```
151
+ spec_ffi=./TON-SDK/target/release/libton_client.dylib
152
+ server_address=http://localhost:80
153
+ giver_abi_name=GiverNodeSE
154
+ giver_amount=10000000000
155
+ ```
156
+ 2. Run tests: inside folder of this library execute this commands
157
+ **rspec spec/binding.rb**
158
+ **rspec spec/client.rb**
159
+ **rspec spec/context.rb**
160
+ **rspec spec/abi.rb**
161
+ **rspec spec/boc.rb**
162
+ **rspec spec/crypto.rb**
163
+ **rspec spec/net.rb**
164
+ **rspec spec/processing.rb**
165
+ **rspec spec/tvm.rb**
166
+ **rspec spec/utils.rb**
167
+
168
+ \n## Update\n\n
169
+ ```\n
170
+ ton-client-ruby update\n
171
+ ```\n\n
172
+ or\n\n
173
+ ```\n
174
+ curl https://raw.githubusercontent.com/tonlabs/TON-SDK/master/tools/api.json > api.json\n\n
175
+ ```\n\n
176
+ ```\n
177
+ ton-client-ruby update ./api.json\n
178
+ ```\n
179
+ or\n\n
180
+ ```\n
181
+ cd ton-client-ruby\n
182
+ ```\n\n
183
+ ```\n
184
+ ./bin/ton-client-ruby update\n
185
+ ```\n
186
+ }
187
+ content = checkContent(content)
188
+ if File.exists?(readmePath)
189
+ File.delete(readmePath)
190
+ end
191
+ File.open(readmePath, 'w+') { |f| f.write(content) }
192
+ end
193
+
194
+ private def checkContent(content)
195
+ content.gsub!(/^([\s]+)# RESPONSE/, "\n\\1# RESPONSE")
196
+ content.gsub(/<Optional>/i, '&lt;Optional&gt;')
197
+ end
198
+
199
+ def customTypes
200
+ content = %{
201
+ - #### #{lib_prefix}MnemonicDictionary
202
+ #{TAB}- case TON = 0
203
+ #{TAB}- case ENGLISH = 1
204
+ #{TAB}- case CHINESE_SIMPLIFIED = 2
205
+ #{TAB}- case CHINESE_TRADITIONAL = 3
206
+ #{TAB}- case FRENCH = 4
207
+ #{TAB}- case ITALIAN = 5
208
+ #{TAB}- case JAPANESE = 6
209
+ #{TAB}- case KOREAN = 7
210
+ #{TAB}- case SPANISH = 8
211
+ }
212
+ content
213
+ end
214
+
215
+ private def checkComment(string, tabs = 1)
216
+ replacedString = "\n"
217
+ tab = ""
218
+ tabs.times do |i|
219
+ tab << TAB
220
+ end
221
+ comment = "#"
222
+ replacedString << "#{tab}#{comment} "
223
+ symbolsWithSpace = "\\.|\\:|\\!|\\?|\\;"
224
+ regxp = /([^#{symbolsWithSpace}])\n/
225
+ result = string
226
+ result.gsub!(/\n+/, "\n")
227
+ result.gsub!(/ \n/, "\n#{comment} ")
228
+ result.gsub!(/(#{symbolsWithSpace})\s*\n/, "\\1#{replacedString}")
229
+ result.gsub!(regxp, "\\1")
230
+ "#{tab}# #{result}"
231
+ end
232
+
233
+ private def generateClientModule(mod, modules)
234
+ content = "module TonClient\n\n#{TAB}class #{mod.name.capitalize}\n#{TAB}#{TAB}include CommonInstanceHelpers\n\n"
235
+ content << "#{TAB}#{TAB}attr_reader :core, :context\n"
236
+ content << "#{TAB}#{TAB}private_accessor "
237
+ modules.each_with_index do |m, i|
238
+ next if m.name.downcase == 'client'
239
+ content << ((modules.size - 1) == i ? ":_#{m.name}\n" : ":_#{m.name}, ")
240
+ end
241
+ content << "#{TAB}#{TAB}MODULE = self.to_s.downcase.gsub(/^(.+::|)(\\w+)$/, '\\2').freeze\n\n"
242
+ content << "#{TAB}#{TAB}def initialize(context: Context.new, core: TonClient::TonBinding)\n"
243
+ content << "#{TAB}#{TAB}#{TAB}@context = context\n"
244
+ content << "#{TAB}#{TAB}#{TAB}@core = core\n#{TAB}#{TAB}end\n\n"
245
+ content << "#{TAB}#{TAB}def destroy_context\n"
246
+ content << "#{TAB}#{TAB}#{TAB}core.tc_destroy_context(context.id)\n#{TAB}#{TAB}end\n\n"
247
+ modules.each_with_index do |m, i|
248
+ next if m.name.downcase == 'client'
249
+ content << "#{TAB}#{TAB}def #{m.name}\n"
250
+ content << "#{TAB}#{TAB}#{TAB}_#{m.name} ||= #{m.name.capitalize}.new(context: context)\n"
251
+ content << "#{TAB}#{TAB}end\n\n"
252
+ end
253
+
254
+ mod.functions.each do |func|
255
+ content << gen_function(func, types)
256
+ end
257
+
258
+ content << "#{TAB}end\n"
259
+ content << "end\n\n"
260
+
261
+ content
262
+ end
263
+
264
+ private def generateModule(mod)
265
+ content = "module TonClient\n\n#{TAB}class #{mod.name.capitalize}\n#{TAB}#{TAB}include CommonInstanceHelpers\n\n"
266
+ content << "#{TAB}#{TAB}attr_reader :core, :context\n"
267
+ content << "#{TAB}#{TAB}MODULE = self.to_s.downcase.gsub(/^(.+::|)(\\w+)$/, '\\2').freeze\n\n"
268
+ content << "#{TAB}#{TAB}def initialize(context: Context.new, core: TonClient::TonBinding)\n"
269
+ content << "#{TAB}#{TAB}#{TAB}@context = context\n"
270
+ content << "#{TAB}#{TAB}#{TAB}@core = core\n"
271
+ content << "#{TAB}#{TAB}end\n\n"
272
+
273
+ mod.functions.each do |func|
274
+ content << gen_function(func, types)
275
+ end
276
+
277
+ content << "#{TAB}end\n"
278
+ content << "end\n\n"
279
+
280
+ content
281
+ end
282
+
283
+ private def gen_function(function, types)
284
+ content = getFunctionComment(function, types)
285
+ content << "#{TAB}#{TAB}def #{function.name}"
286
+ if function.arguments.empty?
287
+ content << "(&block)\n"
288
+ content << "#{TAB}#{TAB}#{TAB}core.requestLibrary(context: context.id, method_name: full_method_name(MODULE, __method__.to_s), payload: {}, &block)\n"
289
+ else
290
+ content << "(payload, &block)\n"
291
+ content << "#{TAB}#{TAB}#{TAB}core.requestLibrary(context: context.id, method_name: full_method_name(MODULE, __method__.to_s), payload: payload, &block)\n"
292
+ end
293
+ content << "#{TAB}#{TAB}end\n\n"
294
+
295
+ content
296
+ end
297
+
298
+ def getFunctionComment(function, types)
299
+ content = ''
300
+ if argument = function.arguments.first
301
+ content << "#{TAB}#{TAB}# INPUT: #{argument.type}\n"
302
+ if types.all_types[argument.type].respond_to?(:fields)
303
+ types.all_types[argument.type].fields.each do |arg|
304
+ content << "#{TAB}#{TAB}# #{arg.name}: #{arg.type} - "
305
+ content << "#{TAB}#{TAB}# #{checkComment(arg.summary, 2)}" if arg.summary
306
+ content << "#{TAB}#{TAB}# #{checkComment(arg.description, 2)}" if arg.description
307
+ content << "\n"
308
+ end
309
+ elsif types.all_types[argument.type].respond_to?(:cases)
310
+ end
311
+ end
312
+
313
+ if types.all_types[function.result]
314
+ content << "#{TAB}#{TAB}# RESPONSE: #{function.result}\n"
315
+ if types.all_types[function.result].respond_to?(:fields)
316
+ types.all_types[function.result].fields.each do |arg|
317
+ content << "#{TAB}#{TAB}# #{arg.name}: #{arg.type} - "
318
+ content << "#{TAB}#{TAB}# #{checkComment(arg.summary, 2)}" if arg.summary
319
+ content << "#{TAB}#{TAB}# #{checkComment(arg.description, 2)}" if arg.description
320
+ content << "\n"
321
+ end
322
+ elsif types.all_types[function.result].respond_to?(:cases)
323
+ end
324
+ end
325
+
326
+ content
327
+ end
328
+ end
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
337
+
338
+
339
+
@@ -0,0 +1,13 @@
1
+ require 'json'
2
+ require 'byebug'
3
+
4
+ module InstanceHelpers
5
+
6
+ def lib_prefix
7
+ ''
8
+ end
9
+
10
+ def lib_enum_postfix
11
+ ''
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ script_file_path = File.expand_path(File.dirname(__FILE__))
4
+
5
+ require "#{script_file_path}/api_converter.rb"
6
+ require "#{script_file_path}/code_generator.rb"
7
+ GEM_DIR = "#{script_file_path}/../.."
8
+
9
+
10
+ `cd #{script_file_path} && curl https://raw.githubusercontent.com/tonlabs/TON-SDK/master/tools/api.json > api.json`
11
+ api_json_path = "#{script_file_path}/../../api.json"
12
+ json = ''
13
+ if File.exists?(api_json_path)
14
+ json = File.read(api_json_path)
15
+ else
16
+ p "File #{api_json_path} is not exist"
17
+ exit 0
18
+ end
19
+ converter = ApiConverter.new(json)
20
+ types = converter.convert
21
+ generator = CodeGenerator.new(types, GEM_DIR)
22
+ generator.generate_self_code
23
+
24
+ version_file = "#{script_file_path}/../everscale-client-ruby/version.rb"
25
+ file = File.read(version_file)
26
+
27
+ p 'check version'
28
+ if file[/VERSION = "(\d+)\.(\d+)\.(\d+)"/]
29
+ major = $1
30
+ minor = $2
31
+ current = $3
32
+ version = "#{major}.#{minor}.#{current.to_i + 1}"
33
+ p version
34
+ data = "module TonClient\n VERSION = \"#{version}\"\nend\n\n"
35
+ p data
36
+ p version_file
37
+ File.open(version_file, 'wb') { |f| f.write(data) }
38
+ p 'update version'
39
+
40
+ puts "make release? Y/N"
41
+ option = gets
42
+ if option.strip.downcase == 'y'
43
+ system(%{cd #{GEM_DIR} && git add .})
44
+ system(%{cd #{GEM_DIR} && git commit -m 'version #{version}'})
45
+ system(%{cd #{GEM_DIR} && bash -lc 'rake release'})
46
+ end
47
+ end
48
+
@@ -0,0 +1,209 @@
1
+ module TonClient
2
+ module TonBinding
3
+ @@request_id = 0
4
+ @@requests = {}
5
+
6
+ class Response
7
+ attr_reader :core
8
+ attr_accessor :result, :error, :custom_response, :finished, :request_id, :current_response
9
+
10
+ def initialize(core: TonClient::TonBinding)
11
+ @core = core
12
+ end
13
+
14
+ def update(request_id, string_data, response_type, finished)
15
+ response_hash = core.read_string_to_hash(string_data)
16
+ self.finished = finished
17
+ self.request_id = request_id
18
+ self.current_response = response_hash
19
+ case response_type
20
+ when 0
21
+ # result
22
+ self.result = response_hash
23
+ when 1
24
+ # error
25
+ self.error = response_hash
26
+ else
27
+ # another
28
+ if response_type >= 100
29
+ self.custom_responses = response_hash
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def self.generate_request_id
36
+ @@request_id = 0 if @@request_id == 4294967295
37
+ @@request_id += 1
38
+ end
39
+
40
+ def self.get_request(id)
41
+ @@requests[id]
42
+ end
43
+
44
+ def self.delete_request(id)
45
+ @@requests[id] = nil
46
+ end
47
+
48
+ def self.set_request(id, &request_block)
49
+ @@requests[id] = request_block
50
+ end
51
+ end
52
+ end
53
+
54
+
55
+ module TonClient
56
+
57
+ module TonBinding
58
+ extend FFI::Library
59
+ ffi_lib FFI::Library::LIBC
60
+
61
+ # memory allocators
62
+ attach_function :malloc, [:size_t], :pointer
63
+ attach_function :calloc, [:size_t], :pointer
64
+ attach_function :valloc, [:size_t], :pointer
65
+ attach_function :realloc, [:pointer, :size_t], :pointer
66
+ attach_function :free, [:pointer], :void
67
+
68
+ # memory movers
69
+ attach_function :memcpy, [:pointer, :pointer, :size_t], :pointer
70
+ attach_function :bcopy, [:pointer, :pointer, :size_t], :void
71
+
72
+ def self.setup_bindings
73
+
74
+ # tc_string_handle_t* tc_create_context(tc_string_data_t config);
75
+ # attach_function :tc_create_context, [TcStringDataT.by_value], TcStringHandleT.by_ref
76
+ attach_function :tc_create_context, [TcStringDataT.by_value], :pointer
77
+
78
+ # fn tc_destroy_context(context: InteropContext)
79
+ attach_function :tc_destroy_context, [:uint32], :void
80
+
81
+ # tc_string_handle_t* tc_request_sync(
82
+ # uint32_t context,
83
+ # tc_string_data_t function_name,
84
+ # tc_string_data_t function_params_json);
85
+ # attach_function :tc_request_sync, [:uint32, TcStringDataT.by_value, TcStringDataT.by_value], TcStringHandleT.by_ref
86
+ attach_function :tc_request_sync, [:uint32, TcStringDataT.by_value, TcStringDataT.by_value], :pointer
87
+
88
+ # enum tc_response_types_t {
89
+ # tc_response_success = 0,
90
+ # tc_response_error = 1,
91
+ # tc_response_nop = 2,
92
+ # tc_response_custom = 100,
93
+ # };
94
+ # typedef void (*tc_response_handler_t)(
95
+ # uint32_t request_id,
96
+ # tc_string_data_t params_json,
97
+ # uint32_t response_type,
98
+ # bool finished);
99
+ callback :tc_response_handler_t, [:uint32, TcStringDataT.by_value, :uint32, :bool], :void
100
+
101
+ # void tc_request(
102
+ # uint32_t context,
103
+ # tc_string_data_t function_name,
104
+ # tc_string_data_t function_params_json,
105
+ # uint32_t request_id,
106
+ # tc_response_handler_t response_handler);
107
+ attach_function :tc_request, [:uint32, TcStringDataT.by_value, TcStringDataT.by_value, :uint32, :tc_response_handler_t], :void
108
+
109
+ # tc_string_data_t tc_read_string(const tc_string_handle_t* handle);
110
+ # attach_function :tc_read_string, [TcStringHandleT.by_ref], TcStringDataT.by_value
111
+ attach_function :tc_read_string, [:pointer], TcStringDataT.by_value
112
+
113
+ # void tc_destroy_string(const tc_string_handle_t* handle)
114
+ # attach_function :tc_destroy_string, [TcStringHandleT.by_ref], :void
115
+ attach_function :tc_destroy_string, [:pointer], :void
116
+ end
117
+
118
+ def self.make_string(string)
119
+ result = TonBinding::TcStringDataT.new
120
+ result[:content] = FFI::MemoryPointer.from_string(string)
121
+ result[:len] = string.bytesize
122
+ result
123
+ end
124
+
125
+ # def self.read_string(tc_string_handle)
126
+ # is_ref = tc_string_handle.class == TonClient::TonBinding::TcStringHandleT
127
+ # if is_ref
128
+ # string = tc_read_string(tc_string_handle)
129
+ # else
130
+ # string = tc_string_handle
131
+ # end
132
+
133
+ # if string[:content].address > 1
134
+ # string = string[:content].read_string(string[:len]).force_encoding('UTF-8') + ''
135
+ # tc_destroy_string(tc_string_handle) if is_ref
136
+ # return string
137
+ # end
138
+ # nil
139
+ # end
140
+
141
+ def self.read_string(tc_string_handle)
142
+ is_ref = tc_string_handle.class == FFI::Pointer
143
+ if is_ref
144
+ string = tc_read_string(tc_string_handle)
145
+ else
146
+ string = tc_string_handle
147
+ end
148
+
149
+ if string[:content].address > 1
150
+ string = string[:content].read_string(string[:len]).force_encoding('UTF-8') + ''
151
+ tc_destroy_string(tc_string_handle) if is_ref
152
+ return string
153
+ end
154
+ nil
155
+ end
156
+
157
+ def self.read_string_to_hash(tc_string_handle_t_ref)
158
+ json_string = read_string(tc_string_handle_t_ref)
159
+ JSON.parse(json_string, {max_nesting: false}) if json_string
160
+ end
161
+
162
+ def self.send_request_sync(context: 1, method_name: '', payload: {})
163
+ raise 'context not found' unless context
164
+ raise 'method_name is empty' if method_name.empty?
165
+
166
+ method_name_string = make_string(method_name)
167
+ payload_string = make_string(payload.to_json)
168
+
169
+ sdk_json_response = tc_request_sync(context, method_name_string, payload_string)
170
+ response = read_string_to_hash(sdk_json_response)
171
+
172
+ return response['result'] if response['result']
173
+ return response['error'] if response['error']
174
+ end
175
+
176
+ def self.send_request(context: 1, method_name: '', payload: {}, request_id: 1, &block)
177
+ raise 'context not found' unless context
178
+ raise 'method_name is empty' if method_name.empty?
179
+
180
+ if block
181
+ method_name_string = make_string(method_name)
182
+ payload_string = make_string(payload.to_json)
183
+ tc_request(context, method_name_string, payload_string, request_id, &block)
184
+ end
185
+ end
186
+
187
+ # block = { |response| }
188
+ def self.requestLibrary(context: 1, method_name: '', payload: {}, &block)
189
+ request_id = generate_request_id
190
+ set_request(request_id, &block)
191
+ send_request(context: context, method_name: method_name, payload: payload, request_id: request_id) do |request_id, string_data, response_type, finished|
192
+ if get_request(request_id)
193
+ response = Response.new
194
+ response.update(request_id, string_data, response_type, finished)
195
+ get_request(request_id).call(response)
196
+ delete_request(request_id) if finished
197
+ end
198
+ end
199
+ end
200
+
201
+ end
202
+ end
203
+
204
+
205
+
206
+
207
+
208
+
209
+
@@ -0,0 +1,21 @@
1
+ module TonClient
2
+ module TonBinding
3
+
4
+ # typedef struct {
5
+ # const char* content;
6
+ # uint32_t len;
7
+ # } tc_string_data_t;
8
+ class TcStringDataT < FFI::Struct
9
+ layout :content, :pointer,
10
+ :len, :uint32
11
+ end
12
+
13
+
14
+ # typedef struct {
15
+ # } tc_string_handle_t;
16
+ class TcStringHandleT < FFI::Struct
17
+ layout :content, :pointer,
18
+ :len, :uint32
19
+ end
20
+ end
21
+ end