Add constants for C enum types. Add NSEnumerator helper. Fix
bugs in Go class names. Add "Frameworks" option to nswrap.toml. Improve handling of ast.EnumDecl and ast.EnumConstantDecl.
This commit is contained in:
parent
9b71889a69
commit
758ea40679
|
@ -1,5 +1,9 @@
|
|||
package ast
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EnumConstantDecl is node represents a enum constant declaration.
|
||||
type EnumConstantDecl struct {
|
||||
Addr Address
|
||||
|
@ -8,6 +12,7 @@ type EnumConstantDecl struct {
|
|||
Referenced bool
|
||||
Name string
|
||||
Type string
|
||||
Type2 string
|
||||
ChildNodes []Node
|
||||
}
|
||||
|
||||
|
@ -16,18 +21,25 @@ func parseEnumConstantDecl(line string) *EnumConstantDecl {
|
|||
`<(?P<position>.*)>
|
||||
( (?P<position2>[^ ]+))?
|
||||
( (?P<referenced>referenced))?
|
||||
(?P<name>.+)
|
||||
'(?P<type>.+?)'`,
|
||||
(?P<name> \w+)
|
||||
(?P<type> '.*?')?
|
||||
(?P<type2>:'.*?')?`,
|
||||
line,
|
||||
)
|
||||
|
||||
type2 := groups["type2"]
|
||||
if type2 != "" {
|
||||
type2 = type2[2 : len(type2)-1]
|
||||
}
|
||||
|
||||
return &EnumConstantDecl{
|
||||
Addr: ParseAddress(groups["address"]),
|
||||
Pos: NewPositionFromString(groups["position"]),
|
||||
Position2: groups["position2"],
|
||||
Referenced: len(groups["referenced"]) > 0,
|
||||
Name: groups["name"],
|
||||
Type: groups["type"],
|
||||
Name: strings.TrimSpace(groups["name"]),
|
||||
Type: removeQuotes(groups["type"]),
|
||||
Type2: type2,
|
||||
ChildNodes: []Node{},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,20 +10,34 @@ type EnumDecl struct {
|
|||
Pos Position
|
||||
Position2 string
|
||||
Name string
|
||||
Type string
|
||||
Type2 string
|
||||
ChildNodes []Node
|
||||
}
|
||||
|
||||
func parseEnumDecl(line string) *EnumDecl {
|
||||
groups := groupsFromRegex(
|
||||
`(?:prev (?P<prev>0x[0-9a-f]+) )?<(?P<position>.*)>(?P<position2> .+:\d+)?(?P<name>.*)`,
|
||||
`(?:prev (?P<prev>0x[0-9a-f]+) )?
|
||||
<(?P<position>.*)>
|
||||
(?P<position2> .+:\d+)?
|
||||
(?P<name> \w+)?
|
||||
(?P<type> '.*?')?
|
||||
(?P<type2>:'.*')?`,
|
||||
line,
|
||||
)
|
||||
|
||||
type2 := groups["type2"]
|
||||
if type2 != "" {
|
||||
type2 = type2[2 : len(type2)-1]
|
||||
}
|
||||
|
||||
return &EnumDecl{
|
||||
Addr: ParseAddress(groups["address"]),
|
||||
Pos: NewPositionFromString(groups["position"]),
|
||||
Position2: groups["position2"],
|
||||
Name: strings.TrimSpace(groups["name"]),
|
||||
Type: removeQuotes(groups["type"]),
|
||||
Type2: type2,
|
||||
ChildNodes: []Node{},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,4 +27,8 @@ func main() {
|
|||
fmt.Println("Length(a2) = ",a2.Count())
|
||||
i1 := a.ObjectAtIndex(1).NSString()
|
||||
fmt.Println(i1.UTF8String())
|
||||
a.ObjectEnumerator().ForIn(func(o *ns.Id) bool {
|
||||
fmt.Println(o.NSString().UTF8String())
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
|
|
@ -19,6 +19,9 @@ Classes = [
|
|||
Functions = [
|
||||
"NSMakeRange",
|
||||
]
|
||||
Enums = [
|
||||
"CF.*",
|
||||
]
|
||||
SysImports = [ "Foundation/Foundation.h" ]
|
||||
Pragma = [ 'clang diagnostic ignored "-Wformat-security"' ]
|
||||
VaArgs = 32
|
||||
|
|
6
main.go
6
main.go
|
@ -21,6 +21,8 @@ type conf struct {
|
|||
InputFiles []string
|
||||
Classes []string
|
||||
Functions []string
|
||||
Enums []string
|
||||
Frameworks []string
|
||||
Imports []string
|
||||
SysImports []string
|
||||
Pragma []string
|
||||
|
@ -177,9 +179,9 @@ func Start() (err error) {
|
|||
|
||||
// build tree
|
||||
tree := buildTree(nodes, 0)
|
||||
//unit := tree[0]
|
||||
w := wrap.NewWrapper(Debug)
|
||||
w.Package = Config.Package
|
||||
w.Frameworks(Config.Frameworks)
|
||||
w.Import(Config.Imports)
|
||||
w.SysImport(Config.SysImports)
|
||||
w.Pragma(Config.Pragma)
|
||||
|
@ -200,6 +202,8 @@ func Start() (err error) {
|
|||
if matches(x.Name,Config.Functions) {
|
||||
w.AddFunction(x)
|
||||
}
|
||||
case *ast.EnumDecl:
|
||||
w.AddEnum(x,Config.Enums)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,7 +157,6 @@ func (t *Type) PointsTo() *Type {
|
|||
}
|
||||
|
||||
func Wrap(s string) {
|
||||
// it is the pointers to this type that get wrapped
|
||||
wrapped[s] = true
|
||||
}
|
||||
|
||||
|
@ -270,11 +269,13 @@ type %s %s
|
|||
}
|
||||
|
||||
func (t *Type) GoInterfaceDecl() string {
|
||||
ct := t.CType()
|
||||
gt := t.GoType()
|
||||
if gt[0] == '*' {
|
||||
gt = gt[1:] // dereference wrapped types
|
||||
ct = ct[:len(ct)-1]
|
||||
}
|
||||
super := Super(gt)
|
||||
super := Super(ct)
|
||||
if super == "" {
|
||||
goInterfaces[gt] = true
|
||||
return fmt.Sprintf(`
|
||||
|
|
217
wrap/main.go
217
wrap/main.go
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -19,9 +20,13 @@ type Wrapper struct {
|
|||
Package string
|
||||
Interfaces map[string]*Interface
|
||||
Functions map[string]*Method
|
||||
NamedEnums map[string]*Enum
|
||||
AnonEnums []*Enum
|
||||
|
||||
cgoFlags strings.Builder // put cGo directives here
|
||||
cCode strings.Builder // put cGo code here
|
||||
goTypes strings.Builder // put Go type declarations here
|
||||
goConst strings.Builder // put Go constants (from C enums) here
|
||||
goCode strings.Builder // put Go code here
|
||||
goHelpers strings.Builder // put Go helper functions here
|
||||
Processed map[string]bool
|
||||
|
@ -34,13 +39,14 @@ func NewWrapper(debug bool) *Wrapper {
|
|||
ret := &Wrapper{
|
||||
Interfaces: map[string]*Interface{},
|
||||
Functions: map[string]*Method{},
|
||||
NamedEnums: map[string]*Enum{},
|
||||
AnonEnums: []*Enum{},
|
||||
Processed: map[string]bool{},
|
||||
VaArgs: 16,
|
||||
}
|
||||
ret.cCode.WriteString(`/*
|
||||
ret.cgoFlags.WriteString(fmt.Sprintf(`/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation
|
||||
`)
|
||||
`))
|
||||
ret.goTypes.WriteString(`
|
||||
type Id struct { }
|
||||
func (o *Id) Ptr() unsafe.Pointer { return unsafe.Pointer(o) }
|
||||
|
@ -48,11 +54,19 @@ func (o *Id) Ptr() unsafe.Pointer { return unsafe.Pointer(o) }
|
|||
return ret
|
||||
}
|
||||
|
||||
func (w *Wrapper) Frameworks(ss []string) {
|
||||
if len(ss) == 0 {
|
||||
return
|
||||
}
|
||||
for _,s := range ss {
|
||||
w.cCode.WriteString(fmt.Sprintf("#import <%s/%s.h>\n",s,s))
|
||||
}
|
||||
w.cgoFlags.WriteString("#cgo LDFLAGS: -framework " + strings.Join(ss," -framework "))
|
||||
}
|
||||
|
||||
func (w *Wrapper) Import(ss []string) {
|
||||
for _,s := range ss {
|
||||
w.cCode.WriteString(`
|
||||
#import "` + s + `"
|
||||
`)
|
||||
w.cCode.WriteString("\n#import \"" + s + "\"\n")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,7 +78,7 @@ func (w *Wrapper) SysImport(ss []string) {
|
|||
|
||||
func (w *Wrapper) Pragma(ss []string) {
|
||||
for _,s := range ss {
|
||||
w.cCode.WriteString("\n#pragma " + s + "\n")
|
||||
w.cgoFlags.WriteString("\n#pragma " + s + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,12 +93,18 @@ type Parameter struct {
|
|||
}
|
||||
|
||||
type Method struct {
|
||||
Name, Class string
|
||||
Name, Class, GoClass string
|
||||
Type *types.Type
|
||||
ClassMethod bool
|
||||
Parameters []*Parameter
|
||||
}
|
||||
|
||||
type Enum struct {
|
||||
Name string
|
||||
Type *types.Type
|
||||
Constants []string
|
||||
}
|
||||
|
||||
//isVoid() returns true if the method has no return value.
|
||||
func (m Method) isVoid() bool {
|
||||
return m.Type.CType() == "void"
|
||||
|
@ -149,6 +169,7 @@ func (w Wrapper) objcparamlist(m *Method) string {
|
|||
//also a C/Objective-C reserved word.
|
||||
var goreserved map[string]bool = map[string]bool{
|
||||
"range": true,
|
||||
"type": true,
|
||||
}
|
||||
|
||||
func (w *Wrapper) gpntp(m *Method) ([]string,[]*types.Type,string) {
|
||||
|
@ -187,7 +208,7 @@ func (w *Wrapper) gpntp(m *Method) ([]string,[]*types.Type,string) {
|
|||
|
||||
|
||||
type Interface struct {
|
||||
Name string
|
||||
Name, GoName string
|
||||
Properties map[string]*Property
|
||||
Methods map[string]*Method
|
||||
}
|
||||
|
@ -247,18 +268,69 @@ func (w *Wrapper) AddFunction(n *ast.FunctionDecl) {
|
|||
w.Functions[n.Name] = m
|
||||
}
|
||||
|
||||
//FIXME: copied from nswrap/main.go, should put this in a utils package
|
||||
func matches(x string, rs []string) bool {
|
||||
for _,r := range rs {
|
||||
if m,_ := regexp.MatchString("^" + r + "$",x); m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *Wrapper) AddEnum(n *ast.EnumDecl,rs []string) {
|
||||
if n.Name != "" && !matches(n.Name,rs) {
|
||||
return
|
||||
}
|
||||
//fmt.Printf("Adding enum: (%s) %s\n",n.Type,n.Name)
|
||||
var tp *types.Type
|
||||
a := (*Avail)(&[]AvailAttr{})
|
||||
if n.Type == "" {
|
||||
tp = nil
|
||||
} else {
|
||||
tp = types.NewTypeFromString(n.Type,"")
|
||||
//fmt.Printf(" type: %s\n",tp.CType())
|
||||
}
|
||||
e := &Enum{
|
||||
Name: n.Name, // NOTE: may be empty string
|
||||
Type: tp,
|
||||
Constants: []string{},
|
||||
}
|
||||
for _,c := range n.Children() {
|
||||
switch x := c.(type) {
|
||||
case *ast.AvailabilityAttr, *ast.UnavailableAttr:
|
||||
a.Add(x)
|
||||
case *ast.EnumConstantDecl:
|
||||
//fmt.Printf("*ast.EnumConstantDecl: (%s) '%s': %s\n",n.Type,n.Name,x.Name)
|
||||
if n.Name == "" && !matches(x.Name,rs) {
|
||||
continue
|
||||
}
|
||||
e.Constants = append(e.Constants,x.Name)
|
||||
}
|
||||
}
|
||||
if a.Available() && len(e.Constants) > 0 {
|
||||
if e.Name == "" {
|
||||
w.AnonEnums = append(w.AnonEnums,e)
|
||||
} else {
|
||||
w.NamedEnums[e.Name] = e
|
||||
}
|
||||
//fmt.Printf(" added\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Wrapper) add(name string, ns []ast.Node) {
|
||||
var i *Interface
|
||||
var ok bool
|
||||
goname := types.NewTypeFromString(name,name).GoType()
|
||||
types.Wrap(goname)
|
||||
if i,ok = w.Interfaces[name]; !ok {
|
||||
i = &Interface{
|
||||
Name: name,
|
||||
GoName: goname,
|
||||
Properties: map[string]*Property{},
|
||||
Methods: map[string]*Method{},
|
||||
}
|
||||
}
|
||||
tp := types.NewTypeFromString(name,name)
|
||||
types.Wrap(tp.GoType())
|
||||
var avail bool
|
||||
for _,c := range ns {
|
||||
switch x := c.(type) {
|
||||
|
@ -278,6 +350,7 @@ func (w *Wrapper) add(name string, ns []ast.Node) {
|
|||
Name: x.Name,
|
||||
Type: types.NewTypeFromString(x.Type,name),
|
||||
Class: name,
|
||||
GoClass: goname,
|
||||
ClassMethod: x.ClassMethod,
|
||||
}
|
||||
//fmt.Println(m.Type.Node.String())
|
||||
|
@ -314,6 +387,7 @@ func (w *Wrapper) add(name string, ns []ast.Node) {
|
|||
m2 := &Method{
|
||||
Name: m.Name,
|
||||
Class: i.Name,
|
||||
GoClass: i.GoName,
|
||||
Type: m.Type.CloneToClass(i.Name),
|
||||
ClassMethod: true,
|
||||
Parameters: []*Parameter{},
|
||||
|
@ -341,12 +415,40 @@ type AvailAttr struct {
|
|||
Deprecated bool
|
||||
}
|
||||
|
||||
type Avail []AvailAttr
|
||||
|
||||
func (a *Avail) Add(n ast.Node) {
|
||||
switch x := n.(type) {
|
||||
case *ast.AvailabilityAttr:
|
||||
*a = append(*a, AvailAttr{
|
||||
OS: x.OS,
|
||||
Version: x.Version,
|
||||
Deprecated: (x.Unknown1 != "0") || x.IsUnavailable,
|
||||
})
|
||||
case *ast.UnavailableAttr:
|
||||
*a = append(*a, AvailAttr{
|
||||
OS: "macos", Deprecated: true })
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Avail) Available() bool {
|
||||
if len(*a) == 0 {
|
||||
return true
|
||||
}
|
||||
for _,x := range *a {
|
||||
if x.OS == "macos" && x.Deprecated == false {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//GetParms returns the parameters of a method declaration and a bool
|
||||
//indicating whether the given method is available on MacOS and not
|
||||
//deprecated.
|
||||
func (w *Wrapper) GetParms(n ast.Node,class string) ([]*Parameter,bool) {
|
||||
ret := make([]*Parameter,0)
|
||||
avail := make([]AvailAttr,0)
|
||||
avail := (*Avail)(&[]AvailAttr{})
|
||||
var parms []string
|
||||
switch x := n.(type) {
|
||||
case *ast.ObjCMethodDecl:
|
||||
|
@ -370,34 +472,14 @@ func (w *Wrapper) GetParms(n ast.Node,class string) ([]*Parameter,bool) {
|
|||
j++
|
||||
case *ast.Variadic:
|
||||
ret[j-1].Type.Variadic = true
|
||||
case *ast.AvailabilityAttr:
|
||||
avail = append(avail,
|
||||
AvailAttr{
|
||||
OS: x.OS,
|
||||
Version: x.Version,
|
||||
Deprecated: x.Unknown1 != "0",
|
||||
})
|
||||
//fmt.Println("AvailAttr ",avail,x)
|
||||
case *ast.UnavailableAttr:
|
||||
avail = append(avail,
|
||||
AvailAttr{ OS: "macos", Deprecated: true })
|
||||
case *ast.AvailabilityAttr, *ast.UnavailableAttr:
|
||||
avail.Add(x)
|
||||
case *ast.Unknown:
|
||||
if Debug { fmt.Printf("GetParms(): ast.Unknown: %s\n",x.Name) }
|
||||
}
|
||||
}
|
||||
// check that the method is available for this OS and not deprecated
|
||||
a := func() bool {
|
||||
if len(avail) == 0 {
|
||||
return true
|
||||
}
|
||||
for _,x := range avail {
|
||||
if x.OS == "macos" && x.Deprecated == false {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}()
|
||||
if !a {
|
||||
if !avail.Available() {
|
||||
return nil, false
|
||||
}
|
||||
// check that we found the right number of parameters
|
||||
|
@ -436,13 +518,17 @@ func (w *Wrapper) processType(tp *types.Type) {
|
|||
if gt == "Char" {
|
||||
w.CharHelpers()
|
||||
}
|
||||
if gt == "NSEnumerator" {
|
||||
w.EnumeratorHelpers()
|
||||
}
|
||||
if bt.IsFunction() {
|
||||
return
|
||||
}
|
||||
super := types.Super(gt)
|
||||
if super != "" {
|
||||
types.Wrap(super)
|
||||
w.processType(types.NewTypeFromString(super,""))
|
||||
tp := types.NewTypeFromString(super,"")
|
||||
types.Wrap(tp.GoType())
|
||||
w.processType(tp)
|
||||
}
|
||||
w.goTypes.WriteString(bt.GoTypeDecl())
|
||||
}
|
||||
|
@ -463,6 +549,15 @@ func (c *Char) String() string {
|
|||
`)
|
||||
}
|
||||
|
||||
func (w *Wrapper) EnumeratorHelpers() {
|
||||
w.goHelpers.WriteString(`
|
||||
func (e *NSEnumerator) ForIn(f func(*Id) bool) {
|
||||
for o := e.NextObject(); o != nil; o = e.NextObject() {
|
||||
if !f(o) { break }
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
func (w *Wrapper) ProcessMethod(m *Method) {
|
||||
w._processMethod(m,false)
|
||||
}
|
||||
|
@ -481,9 +576,9 @@ func (w *Wrapper) _processMethod(m *Method,fun bool) {
|
|||
gname := strings.Title(m.Name)
|
||||
switch {
|
||||
case !m.ClassMethod:
|
||||
gname = "(o *" + m.Class + ") " + gname
|
||||
case m.Type.GoType() != "*" + m.Class:
|
||||
gname = m.Class + gname
|
||||
gname = "(o *" + m.GoClass + ") " + gname
|
||||
case m.Type.GoType() != "*" + m.GoClass:
|
||||
gname = m.GoClass + gname
|
||||
default:
|
||||
lens1 := len(m.Class)
|
||||
i := 0
|
||||
|
@ -492,9 +587,9 @@ func (w *Wrapper) _processMethod(m *Method,fun bool) {
|
|||
if m.Class[i:] == gname[:lens1 - i] { break }
|
||||
}
|
||||
if lens1 - i >= len(gname) {
|
||||
gname = m.Class + gname
|
||||
gname = m.GoClass + gname
|
||||
} else {
|
||||
gname = m.Class + gname[lens1-i:]
|
||||
gname = m.GoClass + gname[lens1-i:]
|
||||
}
|
||||
}
|
||||
cname := m.Name
|
||||
|
@ -511,6 +606,9 @@ func (w *Wrapper) _processMethod(m *Method,fun bool) {
|
|||
if types.IsGoInterface(grtype) {
|
||||
grtype = "*Id"
|
||||
}
|
||||
if gname == grtype { // avoid name conflicts between methods and types
|
||||
gname = "Get" + gname
|
||||
}
|
||||
w.goCode.WriteString(fmt.Sprintf(`
|
||||
//%s
|
||||
func %s(%s) %s {
|
||||
|
@ -538,7 +636,7 @@ func %s(%s) %s {
|
|||
if m.ClassMethod {
|
||||
cobj = m.Class
|
||||
} else {
|
||||
cobj = "(id)o"
|
||||
cobj = "(" + m.Class + "*)o"
|
||||
}
|
||||
cns,cntps := w.cparamlist(m)
|
||||
_ = cns
|
||||
|
@ -563,6 +661,25 @@ func %s(%s) %s {
|
|||
}
|
||||
}
|
||||
|
||||
func (w *Wrapper) ProcessEnum(e *Enum) {
|
||||
gtp := ""
|
||||
if e.Type != nil {
|
||||
gtp = e.Type.GoType()
|
||||
if !w.Processed[gtp] {
|
||||
w.goTypes.WriteString(fmt.Sprintf(`
|
||||
type %s C.%s
|
||||
`,gtp,e.Type.CType()))
|
||||
w.Processed[gtp] = true
|
||||
}
|
||||
}
|
||||
gtp = gtp + " "
|
||||
for _,c := range e.Constants {
|
||||
w.goConst.WriteString(fmt.Sprintf(`
|
||||
const %s %s= C.%s
|
||||
`,c,gtp,c))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Wrapper) Wrap(toproc []string) {
|
||||
if w.Package == "" { w.Package = "ns" }
|
||||
err := os.MkdirAll(w.Package,0755)
|
||||
|
@ -582,6 +699,9 @@ func (w *Wrapper) Wrap(toproc []string) {
|
|||
}
|
||||
//FIXME: sort pInterfaces
|
||||
for _,i := range pInterfaces {
|
||||
if i == nil {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Interface %s: %d properties, %d methods\n",
|
||||
i.Name, len(i.Properties), len(i.Methods))
|
||||
|
||||
|
@ -589,7 +709,7 @@ func (w *Wrapper) Wrap(toproc []string) {
|
|||
func New%s() *%s {
|
||||
return (*%s)(unsafe.Pointer(C.New%s()))
|
||||
}
|
||||
`,i.Name,i.Name,i.Name,i.Name))
|
||||
`,i.GoName,i.GoName,i.GoName,i.Name))
|
||||
|
||||
w.cCode.WriteString(fmt.Sprintf(`
|
||||
%s*
|
||||
|
@ -614,8 +734,16 @@ New%s() {
|
|||
//fmt.Printf("Processing function %s %s\n",m.Type.CType(),m.Name)
|
||||
w.ProcessFunction(m)
|
||||
}
|
||||
for _,e := range w.NamedEnums {
|
||||
w.ProcessEnum(e)
|
||||
}
|
||||
for _,e := range w.AnonEnums {
|
||||
w.ProcessEnum(e)
|
||||
}
|
||||
fmt.Printf("%d functions\n", len(w.Functions))
|
||||
fmt.Printf("%d enums\n", len(w.NamedEnums) + len(w.AnonEnums))
|
||||
of.WriteString("package " + w.Package + "\n\n")
|
||||
of.WriteString(w.cgoFlags.String() + "\n")
|
||||
of.WriteString(w.cCode.String())
|
||||
of.WriteString(`
|
||||
*/
|
||||
|
@ -626,6 +754,7 @@ import (
|
|||
)
|
||||
`)
|
||||
of.WriteString(w.goTypes.String())
|
||||
of.WriteString(w.goConst.String())
|
||||
of.WriteString(w.goHelpers.String())
|
||||
of.WriteString(w.goCode.String())
|
||||
of.Close()
|
||||
|
|
Loading…
Reference in New Issue
Block a user